web-dev-qa-db-fra.com

Comment récupérer des fragments existants en utilisant FragmentPagerAdapter

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.

96
Ismar Slomic

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 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 ...

Problème avec les solutions actuelles: s'appuyer sur un code interne

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.

Solution alternative sans s'appuyer sur tag interne

Voici 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...
}
181
Tony Chan

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:

  1. 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.
  2. Sur la base des explications fournies dans le message prise en charge de la référence des fragments anciens par FragmentPagerAdapterholds , vous devez laisser la création des fragments au 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

80
Ismar Slomic

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 PagerAdapterjavadoc :

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.

12
morgwai

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.

11
personne3000

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;
}
10
Pepijn

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;
}
2
Adam

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;
    }
}
2
netimen

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);
    }
}
2

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);
1
user2740905

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)

0
user1350683

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);
    }
}
0
Alex Miragall

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.

0
TacoEater

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.

0
sbearben