Comment obtenir des fragments existants en utilisant FragmentPagerAdapter
j'ai du mal à faire communiquer mes fragments les uns avec les autres à travers le Activity
, qui utilise le FragmentPagerAdapter
, comme classe d'aide qui implémente la gestion des onglets et tous les détails de connexion d'un ViewPager
avec associé TabHost
. J'ai mis en œuvre FragmentPagerAdapter
tout comme il est fourni par L'Android sample project Support4Demos .
la question principale est Comment puis-je obtenir un fragment particulier à partir de FragmentManager
quand je n'ai ni pièce d'identité ni étiquette? FragmentPagerAdapter
crée les fragments et génère automatiquement L'Id et les Tags.
14 réponses
Résumé du problème
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 sur la récréation D'activité (que ce soit à partir d'une rotation de l'appareil ou le système tuer votre application à ces Fragments
ne seront pas créés à nouveau, mais à la place leurs instances extraites de la FragmentManager
. Maintenant, dites que votre Activity
a besoin d'une référence à ces Fragments
pour travailler dessus. Vous n'avez pas de id
ou tag
pour ceux créés Fragments
parce que FragmentPagerAdapter
mettez-les en interne . Donc, le problème est de savoir comment obtenir une référence sans cette information...
Problème avec les solutions actuelles: en s'appuyant sur le code interne
beaucoup de solutions que j'ai vu sur cette question et des questions similaires reposent sur l'obtention d'une référence à l'existant Fragment
en appelant FragmentManager.findFragmentByTag()
et imitant le étiquette créée à l'interne: "android:switcher:" + viewId + ":" + id
. Le problème avec ça, c'est que tu te fies au code source interne, qui comme nous tous savoir n'est pas garanti de rester le même pour toujours. Les ingénieurs Android de Google pourraient facilement décider de changer la structure tag
qui briserait votre code vous laissant incapable de trouver une référence à l'existant Fragments
.
solution Alternative sans compter sur interne tag
voici un exemple simple de la façon d'obtenir une référence à la Fragments
retourné par FragmentPagerAdapter
qui ne repose pas sur l'interne tags
sur le Fragments
. La clé est de passer outre instantiateItem()
et sauvegardez les références dans à 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 des tags
à la place de membre de la classe des variables/les références à la Fragments
, vous pouvez aussi saisir le tags
, établi par le FragmentPagerAdapter
de la même manière:
REMARQUE: cette 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 l'imitation du tag
interne défini par FragmentPagerAdapter
et utilise à la place APIs appropriés pour les extraire. 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 que selon la conception de votre Activity
, le Fragments
vous essayez de travailler sur Mai ou peut ne pas exister encore, donc vous devez en tenir compte en faisant des null
vérifications avant d'utiliser vos références.
aussi, si à la place vous travaillez avec FragmentStatePagerAdapter
, alors vous ne voulez pas garder des références dures à votre Fragments
parce que vous pourriez avoir beaucoup d'entre eux et des références dures les garderaient inutilement dans la mémoire. Plutôt sauvegardez les références Fragment
dans les variables WeakReference
au lieu des références standard. Comme ceci:
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 leFragmentPagerAdapter
est plutôt trompeur nom de ce que cette méthode ne fait. Il crée de nouveaux fragments, et ne renvoie pas les fragments existants. Dans ce sens, la méthode devrait être renommée à quelque chose commecreateItem(int position)
dans le SDK Android. Cette méthode ne nous aide donc pas à obtenir des fragments. - basé sur l'explication dans le post support FragmentPagerAdapterholds référence à de vieux fragments vous devez laisser la création des fragments à la
FragmentPagerAdapter
et dans le sens où vous n'avez aucune référence aux Fragments ou leurs étiquettes. Si vous avez l'étiquette de fragment, vous pouvez facilement récupérer la référence à partir duFragmentManager
en appelantfindFragmentByTag()
. Nous avons besoin d'un moyen pour trouver l'étiquette d'un fragment à la position donnée de la page.
Solution
ajouter la méthode d'aide suivante dans votre classe pour récupérer l'étiquette de fragment et l'Envoyer à la méthode findFragmentByTag()
.
private String getFragmentTag(int viewPagerId, int fragmentPosition)
{
return "android:switcher:" + viewPagerId + ":" + fragmentPosition;
}
NOTE! C'est la même méthode que FragmentPagerAdapter
utilise pour créer 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
j'ai créé cette méthode qui fonctionne 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;
}
la façon dont je l'ai fait est défini un Hashtable de faibles références comme suit:
protected Hashtable<Integer, WeakReference<Fragment>> fragmentReferences;
puis 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;
}
alors 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
trick, car il ne repose pas sur la façon dont le FragmentPagerAdapter est mis en œuvre. Bien sûr, si le fragment a a été libéré par FragmentPagerAdapter ou s'il n'a pas encore été créé, getFragment retournera null.
si quelqu'un trouve quelque chose qui ne va pas avec cette approche, les commentaires sont plus que bienvenus.
vous n'avez pas besoin de surcharger instantiateItem
ni de compter sur la compatibilité de créer des étiquettes de fragment avec la méthode interne makeFragmentName
. instantiateItem
est une public méthode de sorte que vous pouvez (et en fait vous devrait ) l'appeler dans onCreate
méthode de votre activité pour obtenir des références à des instances de vos fragments et de les stocker sur les var locales si vous avez besoin. N'oubliez pas d'entourer un ensemble d'appels instantiateItem
avec
startUpdate
et finishUpdate
méthodes décrites dans PagerAdapter
javadoc :
un appel à la mise à jour de la méthode PagerAdapter(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).
donc pour exemple c'est la façon de stocker des références à vos fragments d'onglet dans onCreate
méthode:
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 tabLayout = (TabLayout) findViewById(R.id.tabs);
tabLayout.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 fragment existantes de FragmentManager
. Seulement s'ils n'existent pas encore, il en créera de nouveaux en utilisant la méthode getItem
de votre adaptateur et les stockera dans le FragmentManager
pour une utilisation future.
quelques informations supplémentaires:
Si vous n'appelez pas instantiateItem
entouré par startUpdate
/ finishUpdate
dans votre méthode onCreate
alors vous risquez que vos instances de fragments ne seront jamais commis à FragmentManager
: lorsque votre activité devient premier plan instantiateItem
sera appelé automatiquement pour obtenir vos fragments, mais startUpdate
/ finishUpdate
pas (selon les détails de la mise en œuvre) et ce qu'ils font essentiellement est commencer / commettre un FragmentTransaction
.
Ce peut avoir pour résultat que les références aux instances de fragment créées soient perdues très rapidement (par exemple lorsque vous tournez votre écran) et recréées beaucoup plus souvent que nécessaire. Selon la "lourdeur" de vos fragments, cela peut avoir des conséquences non négligeables sur la performance.
Plus important encore, dans de tels cas, les fragments stockés sur les var locales deviendront périmés: android plate-forme n'a pas été en mesure d'obtenir les mêmes instances à partir de FragmentManager
il peut créer et utiliser de nouvelles instances, alors que vos vars seront encore référencement des anciennes.
la solution suggérée par @personne3000 est sympa, mais elle a un problème: quand l'activité passe à l'arrière-plan et se fait tuer par le système (afin d'obtenir un peu de mémoire libre) puis restaurée, la fragmentReferences
sera vide, parce que getItem
ne s'appellerait pas.
la classe ci-dessous s'occupe d'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;
}
}
le principal obstacle à l'obtention d'une poignée aux 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 plus appelé.
Voici une approche qui ne repose pas sur la mise en œuvre de FragmentPagerAdapter pour obtenir l'étiquette. Override instantiateItem() qui renvoie le fragment créé à partir de getItem () ou trouvé à 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;
}
j'utilise toujours cette classe de base, quand j'ai besoin d'accéder à des fragments d'enfant ou primaire (actuellement visible) fragment. Il ne s'appuie sur aucun détail d'implémentation et s'occupe des changements de cycle de vie, parce que les méthodes écrasées sont appelées dans les deux cas - quand une nouvelle instance de fragment est créée et quand 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 ID au lieu de tags. (J'utilise J'ai défini FragmentStatePagerAdapter qui utilise mes Fragments personnalisés dans lesquels j'ai surmonté la méthode onAttach, où vous sauvegardez l'id quelque part:
@Override
public void onAttach(Context context){
super.onAttach(context);
MainActivity.fragId = getId();
}
et ensuite vous accédez simplement au fragment facilement à l'intérieur de l'activité:
Fragment f = getSupportFragmentManager.findFragmentById(fragId);
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 retourné null ou causé le plantage de l'application.
j'ai remarqué que sur la rotation de l'écran le fragment était attaché, donc je l'ai utilisé pour renvoyer le fragment à 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 en activité onCreate ():
if (savedInstanceState != null) {
if (mListFragment != null)
mListFragment.setListItems(items);
}
cette approche attache le fragment visible réel à l'activité sans en créer un nouveau.
Je ne suis pas sûr que ma méthode était la bonne ou la meilleure façon de le faire car je suis un débutant relatif avec 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'avais une activité d'hébergement qui utilisait un ViewPager avec un FragmentStatePagerAdapter. Pour 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 Hasetlié pour garder la trace des 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 I a ajouté les fragments à la liste dans onAttach et les a retirés dans 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 le cadre de l'activité d'hébergement, vous pourrez désormais utiliser mFragments pour itérer à travers les fragments qui existent actuellement dans le FragmentStatePagerAdapter.
cette classe fait l'affaire sans s'appuyer sur des étiquettes internes. Attention: les Fragments doivent être accessibles en utilisant la méthode getFragment et non la méthode 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);
}
}
essayez ce code,
public class MYFragmentPAdp extends FragmentPagerAdapter {
public MYFragmentPAdp(FragmentManager fm) {
super(fm);
}
@Override
public int getCount() {
return 2;
}
@Override
public Fragment getItem(int position) {
if (position == 0)
Fragment fragment = new Fragment1();
else (position == 1)
Fragment fragment = new Fragment2();
return fragment;
}
}