J'ai du mal à faire communiquer mes fragments entre eux via le Activity
, qui utilise le FragmentPagerAdapter
, en tant que classe d'assistance implémentant la gestion des onglets et tous les détails de la connexion d'un ViewPager
avec TabHost
associé. J'ai implémenté FragmentPagerAdapter
de la même manière que le Android exemple de projet Support4Demos.
La question principale est comment puis-je obtenir un fragment particulier de FragmentManager
quand je n'ai ni id ni tag? FragmentPagerAdapter
crée les fragments et génère automatiquement l'ID et les balises.
Note: Dans cette réponse, je vais faire référence à FragmentPagerAdapter
et à son code source. Mais la solution générale devrait également s'appliquer à FragmentStatePagerAdapter
.
Si vous lisez ceci, vous savez probablement déjà que FragmentPagerAdapter
/FragmentStatePagerAdapter
est destiné à créer Fragments
pour votre ViewPager
, mais lors de la recréation d’activité ( une rotation de l'appareil ou le système tue votre application pour qu'elle regagne de la mémoire) ces Fragments
ne seront pas créés à nouveau, mais leurs instances extraites de la FragmentManager
. Maintenant, supposons que votre Activity
ait besoin d'une référence à ces Fragments
pour pouvoir y travailler. Vous n'avez ni id
ni tag
pour ceux-ci créés Fragments
car FragmentPagerAdapter
les a définis en interne . Le problème est donc de savoir comment obtenir une référence sans ces informations ...
Un grand nombre des solutions que j'ai vues à ce sujet et à des questions similaires reposent sur l'obtention d'une référence à l'existant Fragment
en appelant FragmentManager.findFragmentByTag()
et en imitant le balise créée en interne: "Android:switcher:" + viewId + ":" + id
. Le problème, c’est que vous vous fiez au code source interne, qui, comme nous le savons tous, n’a pas toujours la garantie de rester identique. Les ingénieurs Android de Google pourraient facilement décider de modifier la structure tag
, ce qui endommagerait votre code, ce qui vous empêcherait de trouver une référence au Fragments
existant.
tag
interneVoici un exemple simple sur la façon d'obtenir une référence au Fragments
renvoyé par FragmentPagerAdapter
qui ne repose pas sur le tags
interne défini sur le Fragments
. La clé est de remplacer instantiateItem()
et d'enregistrer les références dans cet élément à la place de dans getItem()
.
public class SomeActivity extends Activity {
private FragmentA m1stFragment;
private FragmentB m2ndFragment;
// other code in your Activity...
private class CustomPagerAdapter extends FragmentPagerAdapter {
// other code in your custom FragmentPagerAdapter...
public CustomPagerAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
// Do NOT try to save references to the Fragments in getItem(),
// because getItem() is not always called. If the Fragment
// was already created then it will be retrieved from the FragmentManger
// and not here (i.e. getItem() won't be called again).
switch (position) {
case 0:
return new FragmentA();
case 1:
return new FragmentB();
default:
// This should never happen. Always account for each position above
return null;
}
}
// Here we can finally safely save a reference to the created
// Fragment, no matter where it came from (either getItem() or
// FragmentManger). Simply save the returned Fragment from
// super.instantiateItem() into an appropriate reference depending
// on the ViewPager position.
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
// save the appropriate reference depending on position
switch (position) {
case 0:
m1stFragment = (FragmentA) createdFragment;
break;
case 1:
m2ndFragment = (FragmentB) createdFragment;
break;
}
return createdFragment;
}
}
public void someMethod() {
// do work on the referenced Fragments, but first check if they
// even exist yet, otherwise you'll get an NPE.
if (m1stFragment != null) {
// m1stFragment.doWork();
}
if (m2ndFragment != null) {
// m2ndFragment.doSomeWorkToo();
}
}
}
ou si vous préférez travailler avec tags
à la place des variables/références de membre de la classe à la Fragments
, vous pouvez également récupérer le tags
défini par FragmentPagerAdapter
de la même manière: NOTE: ceci ne s'applique pas à FragmentStatePagerAdapter
car il ne définit pas tags
lors de la création de son Fragments
.
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment createdFragment = (Fragment) super.instantiateItem(container, position);
// get the tags set by FragmentPagerAdapter
switch (position) {
case 0:
String firstTag = createdFragment.getTag();
break;
case 1:
String secondTag = createdFragment.getTag();
break;
}
// ... save the tags somewhere so you can reference them later
return createdFragment;
}
Notez que cette méthode ne repose PAS sur la réplication de la tag
interne définie par FragmentPagerAdapter
et utilise plutôt les API appropriées pour les récupérer. De cette façon, même si le tag
change dans les versions futures du SupportLibrary
, vous serez toujours en sécurité.
N'oubliez pas qu'en fonction du design de votre Activity
, le Fragments
sur lequel vous essayez de travailler peut exister ou non, vous devez donc en rendre compte en vérifiant null
avant d’utiliser vos références.
De plus, si au lieu de cela vous travaillez avec FragmentStatePagerAdapter
, vous ne voulez pas conserver de références strictes à votre Fragments
parce que vous pourriez en avoir beaucoup et que les références en dur les garderaient inutilement en mémoire. Enregistrez plutôt les références Fragment
dans les variables WeakReference
à la place des variables standard. Comme ça:
WeakReference<Fragment> m1stFragment = new WeakReference<Fragment>(createdFragment);
// ...and access them like so
Fragment firstFragment = m1stFragment.get();
if (firstFragment != null) {
// reference hasn't been cleared yet; do work...
}
J'ai trouvé la réponse à ma question basée sur le post suivant: réutiliser des fragments dans un fragmentpageradapter
Peu de choses que j'ai apprises:
getItem(int position)
dans le FragmentPagerAdapter
est un nom plutôt trompeur sur ce que fait réellement cette méthode. Il crée de nouveaux fragments, ne renvoie pas ceux existants. Dans ce sens, la méthode devrait être renommée quelque chose comme createItem(int position)
dans le SDK Android). Cette méthode ne nous aide donc pas à obtenir des fragments.FragmentPagerAdapter
, ce qui signifie que vous n’avez aucune référence aux fragments ni à leurs balises. Si vous avez une balise fragment, vous pouvez facilement récupérer une référence à partir de FragmentManager
en appelant findFragmentByTag()
. Nous avons besoin d'un moyen de trouver la balise d'un fragment à une position de page donnée.Solution
Ajoutez la méthode d'assistance suivante dans votre classe pour récupérer la balise fragment et l'envoyer à la méthode findFragmentByTag()
.
private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
return "Android:switcher:" + viewPagerId + ":" + fragmentPosition;
}
REMARQUE! Cette méthode est identique à celle utilisée par FragmentPagerAdapter
lors de la création de nouveaux fragments. Voir ce lien http://code.google.com/p/openintents/source/browse/trunk/compatibility/AndroidSupportV2/src/Android/support/v2/app/FragmentPagerAdapter.Java#104
vous n'avez pas besoin de remplacer instantiateItem
ni de vous appuyer sur la compatibilité de la création de balises fragment avec la méthode interne makeFragmentName
. instantiateItem
est une méthode publique afin que vous puissiez (et en réalité vous devriez ) l'appeler dans onCreate
méthode de votre activité pour obtenir des références aux instances de vos fragments et les stocker sur des vars locaux si vous en avez besoin. Rappelez-vous simplement d’entourer un ensemble de instantiateItem
appels avec les méthodes startUpdate
et finishUpdate
comme décrit dans PagerAdapter
javadoc :
Un appel à la méthode PagerAdapter startUpdate (ViewGroup) indique que le contenu du ViewPager est sur le point de changer. Un ou plusieurs appels à instantiateItem (ViewGroup, int) et/ou destroyItem (ViewGroup, int, Object) suivront et la fin d'une mise à jour sera signalée par un appel à finishUpdate (ViewGroup).
Ainsi, par exemple, c’est le moyen de stocker les références à vos fragments d’onglet dans la méthode onCreate
:
public class MyActivity extends AppCompatActivity {
Fragment0 tab0; Fragment1 tab1;
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.myLayout);
ViewPager viewPager = (ViewPager) findViewById(R.id.myViewPager);
MyPagerAdapter adapter = new MyPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(adapter);
((TabLayout) findViewById(R.id.tabs)).setupWithViewPager(viewPager);
adapter.startUpdate(viewPager);
tab0 = (Fragment0) adapter.instantiateItem(viewPager, 0);
tab1 = (Fragment1) adapter.instantiateItem(viewPager, 1);
adapter.finishUpdate(viewPager);
}
class MyPagerAdapter extends FragmentPagerAdapter {
public MyPagerAdapter(FragmentManager manager) {super(manager);}
@Override public int getCount() {return 2;}
@Override public Fragment getItem(int position) {
if (position == 0) return new Fragment0();
if (position == 1) return new Fragment1();
return null; // or throw some exception
}
@Override public CharSequence getPageTitle(int position) {
if (position == 0) return getString(R.string.tab0);
if (position == 1) return getString(R.string.tab1);
return null; // or throw some exception
}
}
}
instantiateItem
va d'abord essayer d'obtenir des références aux instances de fragments existantes à partir de FragmentManager
. Seulement s'ils n'existent pas encore, il en créera de nouveaux à l'aide de la méthode getItem
de votre adaptateur et les "stockera" dans la FragmentManager
pour toute utilisation future.
Quelques informations supplémentaires:
Si vous n'appelez pas instantiateItem
entouré de startUpdate
/finishUpdate
dans votre méthode onCreate
, vous risquez alors que vos instances de fragments ne jamais être commis à FragmentManager
: lorsque votre activité sera au premier plan, instantiateItem
sera appelé automatiquement pour obtenir vos fragments, mais startUpdate
/finishUpdate
peut pas (selon les détails de l'implémentation) et ce qu'ils font fondamentalement est de commencer/commettre un FragmentTransaction
.
Ceci peut-être entraîne la perte de références aux instances de fragments créées très rapidement (par exemple lorsque vous faites pivoter votre écran) et de les recréer beaucoup plus souvent que nécessaire. En fonction de la "lourdeur" de vos fragments, cela peut avoir des conséquences non négligeables sur les performances.
Plus important encore, dans ce cas, les instances de fragments stockés sur les vars locaux deviendront obsolètes: depuis Android n'a pas pu obtenir les mêmes instances de FragmentManager
il peut en créer et en utiliser de nouveaux, alors que vos vars continueront à référencer les anciens.
La façon dont je l'ai fait est de définir une table de hachage de WeakReferences comme suit:
protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;
Ensuite, j'ai écrit la méthode getItem () comme ceci:
@Override
public Fragment getItem(int position) {
Fragment fragment;
switch(position) {
case 0:
fragment = new MyFirstFragmentClass();
break;
default:
fragment = new MyOtherFragmentClass();
break;
}
fragmentReferences.put(position, new WeakReference<Fragment>(fragment));
return fragment;
}
Ensuite, vous pouvez écrire une méthode:
public Fragment getFragment(int fragmentId) {
WeakReference<Fragment> ref = fragmentReferences.get(fragmentId);
return ref == null ? null : ref.get();
}
Cela semble bien fonctionner et je le trouve un peu moins hacky que le
"Android:switcher:" + viewId + ":" + position
l’astuce car elle ne dépend pas de la manière dont FragmentPagerAdapter est implémenté. Bien sûr, si le fragment a été publié par FragmentPagerAdapter ou s'il n'a pas encore été créé, getFragment renverra la valeur null.
Si quelqu'un trouve quelque chose qui ne va pas avec cette approche, les commentaires sont plus que bienvenus.
J'ai créé cette méthode qui fonctionne pour moi pour obtenir une référence au fragment actuel.
public static Fragment getCurrentFragment(ViewPager pager, FragmentPagerAdapter adapter) {
try {
Method m = adapter.getClass().getSuperclass().getDeclaredMethod("makeFragmentName", int.class, long.class);
Field f = adapter.getClass().getSuperclass().getDeclaredField("mFragmentManager");
f.setAccessible(true);
FragmentManager fm = (FragmentManager) f.get(adapter);
m.setAccessible(true);
String tag = null;
tag = (String) m.invoke(null, pager.getId(), (long) pager.getCurrentItem());
return fm.findFragmentByTag(tag);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
return null;
}
Le principal obstacle à la gestion des fragments est que vous ne pouvez pas compter sur getItem (). Après un changement d'orientation, les références aux fragments seront nulles et getItem () n'est pas appelé à nouveau.
Voici une approche qui ne repose pas sur l'implémentation de FragmentPagerAdapter pour obtenir le tag. Remplacez instantiateItem () qui renverra le fragment créé à partir de getItem () ou à partir du gestionnaire de fragments.
@Override
public Object instantiateItem(ViewGroup container, int position) {
Object value = super.instantiateItem(container, position);
if (position == 0) {
someFragment = (SomeFragment) value;
} else if (position == 1) {
anotherFragment = (AnotherFragment) value;
}
return value;
}
la solution suggérée par @ personne3000 est bien Nice, mais il y a un problème: quand l'activité passe en arrière-plan et est tuée par le système (afin d'avoir de la mémoire libre) puis restaurée, le fragmentReferences
sera vide. , parce que getItem
ne serait pas appelé.
La classe ci-dessous gère une telle situation:
public abstract class AbstractHolderFragmentPagerAdapter<F extends Fragment> extends FragmentPagerAdapter {
public static final String FRAGMENT_SAVE_PREFIX = "holder";
private final FragmentManager fragmentManager; // we need to store fragment manager ourselves, because parent's field is private and has no getters.
public AbstractHolderFragmentPagerAdapter(FragmentManager fm) {
super(fm);
fragmentManager = fm;
}
private SparseArray<WeakReference<F>> holder = new SparseArray<WeakReference<F>>();
protected void holdFragment(F fragment) {
holdFragment(holder.size(), fragment);
}
protected void holdFragment(int position, F fragment) {
if (fragment != null)
holder.put(position, new WeakReference<F>(fragment));
}
public F getHoldedItem(int position) {
WeakReference<F> ref = holder.get(position);
return ref == null ? null : ref.get();
}
public int getHolderCount() {
return holder.size();
}
@Override
public void restoreState(Parcelable state, ClassLoader loader) { // code inspired by Google's FragmentStatePagerAdapter implementation
super.restoreState(state, loader);
Bundle bundle = (Bundle) state;
for (String key : bundle.keySet()) {
if (key.startsWith(FRAGMENT_SAVE_PREFIX)) {
int index = Integer.parseInt(key.substring(FRAGMENT_SAVE_PREFIX.length()));
Fragment f = fragmentManager.getFragment(bundle, key);
holdFragment(index, (F) f);
}
}
}
@Override
public Parcelable saveState() {
Bundle state = (Bundle) super.saveState();
if (state == null)
state = new Bundle();
for (int i = 0; i < holder.size(); i++) {
int id = holder.keyAt(i);
final F f = getHoldedItem(i);
String key = FRAGMENT_SAVE_PREFIX + id;
fragmentManager.putFragment(state, key, f);
}
return state;
}
}
J'utilise toujours cette classe de base lorsque j'ai besoin d'accéder à des fragments enfants ou à des fragments primaires (actuellement visibles). Il ne repose sur aucun détail d'implémentation et prend en charge les modifications de cycle de vie, car les méthodes écrasées sont appelées dans les deux cas: lorsqu'une nouvelle instance de fragment est créée et lorsque l'instance est reçue de FragmentManager.
public abstract class FragmentPagerAdapterExt extends FragmentPagerAdapter {
private final ArrayList<Fragment> mFragments;
private Fragment mPrimaryFragment;
public FragmentPagerAdapterExt(FragmentManager fm) {
super(fm);
mFragments = new ArrayList<>(getCount());
}
@Override public Object instantiateItem(ViewGroup container, int position) {
Object object = super.instantiateItem(container, position);
mFragments.add((Fragment) object);
return object;
}
@Override public void destroyItem(ViewGroup container, int position, Object object) {
mFragments.remove(object);
super.destroyItem(container, position, object);
}
@Override public void setPrimaryItem(ViewGroup container, int position, Object object) {
super.setPrimaryItem(container, position, object);
mPrimaryFragment = (Fragment) object;
}
/** Returns currently visible (primary) fragment */
public Fragment getPrimaryFragment() {
return mPrimaryFragment;
}
/** Returned list can contain null-values for not created fragments */
public List<Fragment> getFragments() {
return Collections.unmodifiableList(mFragments);
}
}
J'ai réussi à résoudre ce problème en utilisant des identifiants au lieu de balises. (J'utilise J'ai défini FragmentStatePagerAdapter qui utilise mes fragments personnalisés dans lesquels j'ai surchargé la méthode onAttach, où vous enregistrez l'id quelque part:
@Override
public void onAttach(Context context){
super.onAttach(context);
MainActivity.fragId = getId();
}
Et puis vous accédez simplement au fragment facilement dans l’activité:
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
Voir cet article pour le retour des fragments de FragmentPagerAdapter. Est-ce que vous devez connaître l'index de votre fragment - mais cela serait défini dans getItem () (à l'instanciation uniquement)
Cette classe fait l'affaire sans compter sur les balises internes. Avertissement: Les fragments doivent être accessibles à l'aide de la méthode getFragment et non de celle de getItem.
public class ViewPagerAdapter extends FragmentPagerAdapter {
private final Map<Integer, Reference<Fragment>> fragments = new HashMap<>();
private final List<Callable0<Fragment>> initializers = new ArrayList<>();
private final List<String> titles = new ArrayList<>();
public ViewPagerAdapter(FragmentManager fm) {
super(fm);
}
void addFragment(Callable0<Fragment> initializer, String title) {
initializers.add(initializer);
titles.add(title);
}
public Optional<Fragment> getFragment(int position) {
return Optional.ofNullable(fragments.get(position).get());
}
@Override
public Fragment getItem(int position) {
Fragment fragment = initializers.get(position).execute();
return fragment;
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
fragments.put(position, new WeakReference<>(fragment));
return fragment;
}
@Override
public int getCount() {
return initializers.size();
}
@Override
public CharSequence getPageTitle(int position) {
return titles.get(position);
}
}
Je ne sais pas si c'est la meilleure approche mais rien d'autre n'a fonctionné pour moi. Toutes les autres options, y compris getActiveFragment, ont renvoyé la valeur null ou ont entraîné le blocage de l'application.
J'ai remarqué que lors de la rotation de l'écran, le fragment était en cours de fixation, je l'ai donc utilisé pour le renvoyer à l'activité.
Dans le fragment:
@Override
public void onAttach(Activity activity) {
super.onAttach(activity);
try {
mListener = (OnListInteractionListener) activity;
mListener.setListFrag(this);
} catch (ClassCastException e) {
throw new ClassCastException(activity.toString()
+ " must implement OnFragmentInteractionListener");
}
}
Puis dans l'activité:
@Override
public void setListFrag(MyListFragment lf) {
if (mListFragment == null) {
mListFragment = lf;
}
}
Et enfin, dans l’activité onCreate ():
if (savedInstanceState != null) {
if (mListFragment != null)
mListFragment.setListItems(items);
}
Cette approche associe le fragment visible réel à l'activité sans en créer un nouveau.
Je ne suis pas sûr que ma méthode soit la bonne ou la meilleure façon de le faire car je suis un débutant relatif à Java/Android, mais cela a fonctionné (je suis sûr que cela viole les principes orientés objet mais aucune autre solution n'a fonctionné pour mon cas d'utilisation).
J'ai eu une activité d'hébergement qui utilisait un ViewPager avec un FragmentStatePagerAdapter. Afin d'obtenir des références aux fragments créés par FragmentStatePagerAdapter, j'ai créé une interface de rappel dans la classe fragment:
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
Dans l'activité d'hébergement, j'ai implémenté l'interface et créé un LinkedHasSet pour suivre les fragments:
public class HostingActivity extends AppCompatActivity implements ViewPagerFragment.Callbacks {
private LinkedHashSet<Fragment> mFragments = new LinkedHashSet<>();
@Override
public void addFragment (Fragment fragment) {
mFragments.add(fragment);
}
@Override
public void removeFragment (Fragment fragment) {
mFragments.remove(fragment);
}
}
Dans la classe ViewPagerFragment, j'ai ajouté les fragments à la liste de onAttach et les ai supprimés de onDetach:
public class ViewPagerFragment extends Fragment {
private Callbacks mCallbacks;
public interface Callbacks {
public void addFragment (Fragment fragment);
public void removeFragment (Fragment fragment);
}
@Override
public void onAttach (Context context) {
super.onAttach(context);
mCallbacks = (Callbacks) context;
// Add this fragment to the HashSet in the hosting activity
mCallbacks.addFragment(this);
}
@Override
public void onDetach() {
super.onDetach();
// Remove this fragment from the HashSet in the hosting activity
mCallbacks.removeFragment(this);
mCallbacks = null;
}
}
Dans l'activité d'hébergement, vous pourrez désormais utiliser mFragments pour parcourir les fragments qui existent actuellement dans FragmentStatePagerAdapter.