Question ViewPager et fragments - quelle est la bonne manière de stocker l'état des fragments?


Les fragments semblent très intéressants pour la séparation de la logique de l'interface utilisateur dans certains modules. Mais avec ViewPager son cycle de vie est encore brumeux pour moi. Les pensées du gourou sont donc grandement nécessaires!

modifier

Voir la solution stupide ci-dessous ;-)

Portée

L'activité principale a un ViewPager avec des fragments. Ces fragments pourraient implémenter une logique légèrement différente pour d'autres activités (sous-marines), de sorte que les données des fragments sont remplies via une interface de rappel dans l'activité. Et tout fonctionne bien au premier lancement, mais! ...

Problème

Lorsque l'activité est recréée (par exemple, lors d'un changement d'orientation), le ViewPagerLes fragments. Le code (que vous trouverez ci-dessous) indique que chaque fois que l'activité est créée, j'essaie de créer une nouvelle ViewPager l'adaptateur de fragments est le même que des fragments (peut-être c'est le problème) mais FragmentManager a déjà tous ces fragments stockés quelque part (où?) et commence le mécanisme de récréation pour ceux-ci. Ainsi, le mécanisme de recréation appelle onAttach, onCreateView, etc. du fragment «ancien» avec mon appel d'interface de rappel pour initier des données via la méthode implémentée de l'activité. Mais cette méthode pointe vers le fragment nouvellement créé qui est créé via la méthode onCreate de l'activité.

Problème

Peut-être que j'utilise de mauvais modèles, mais même Android 3 Pro livre n'a pas beaucoup à ce sujet. Alors, S'il vous plaît, donnez-moi un coup de poing un-deux et de souligner comment le faire de la bonne façon. Merci beaucoup!

Code

Activité principale

public class DashboardActivity extends BasePagerActivity implements OnMessageListActionListener {

private MessagesFragment mMessagesFragment;

@Override
protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    setContentView(R.layout.viewpager_container);
    new DefaultToolbar(this);

    // create fragments to use
    mMessagesFragment = new MessagesFragment();
    mStreamsFragment = new StreamsFragment();

    // set titles and fragments for view pager
    Map<String, Fragment> screens = new LinkedHashMap<String, Fragment>();
    screens.put(getApplicationContext().getString(R.string.dashboard_title_dumb), new DumbFragment());
    screens.put(getApplicationContext().getString(R.string.dashboard_title_messages), mMessagesFragment);

    // instantiate view pager via adapter
    mPager = (ViewPager) findViewById(R.id.viewpager_pager);
    mPagerAdapter = new BasePagerAdapter(screens, getSupportFragmentManager());
    mPager.setAdapter(mPagerAdapter);

    // set title indicator
    TitlePageIndicator indicator = (TitlePageIndicator) findViewById(R.id.viewpager_titles);
    indicator.setViewPager(mPager, 1);

}

/* set of fragments callback interface implementations */

@Override
public void onMessageInitialisation() {

    Logger.d("Dash onMessageInitialisation");
    if (mMessagesFragment != null)
        mMessagesFragment.loadLastMessages();
}

@Override
public void onMessageSelected(Message selectedMessage) {

    Intent intent = new Intent(this, StreamActivity.class);
    intent.putExtra(Message.class.getName(), selectedMessage);
    startActivity(intent);
}

BasePagerActivity aka Assistant

public class BasePagerActivity extends FragmentActivity {

BasePagerAdapter mPagerAdapter;
ViewPager mPager;
}

Adaptateur

public class BasePagerAdapter extends FragmentPagerAdapter implements TitleProvider {

private Map<String, Fragment> mScreens;

public BasePagerAdapter(Map<String, Fragment> screenMap, FragmentManager fm) {

    super(fm);
    this.mScreens = screenMap;
}

@Override
public Fragment getItem(int position) {

    return mScreens.values().toArray(new Fragment[mScreens.size()])[position];
}

@Override
public int getCount() {

    return mScreens.size();
}

@Override
public String getTitle(int position) {

    return mScreens.keySet().toArray(new String[mScreens.size()])[position];
}

// hack. we don't want to destroy our fragments and re-initiate them after
@Override
public void destroyItem(View container, int position, Object object) {

    // TODO Auto-generated method stub
}

}

Fragment

public class MessagesFragment extends ListFragment {

private boolean mIsLastMessages;

private List<Message> mMessagesList;
private MessageArrayAdapter mAdapter;

private LoadMessagesTask mLoadMessagesTask;
private OnMessageListActionListener mListener;

// define callback interface
public interface OnMessageListActionListener {
    public void onMessageInitialisation();
    public void onMessageSelected(Message selectedMessage);
}

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    // setting callback
    mListener = (OnMessageListActionListener) activity;
    mIsLastMessages = activity instanceof DashboardActivity;

}

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    inflater.inflate(R.layout.fragment_listview, container);
    mProgressView = inflater.inflate(R.layout.listrow_progress, null);
    mEmptyView = inflater.inflate(R.layout.fragment_nodata, null);
    return super.onCreateView(inflater, container, savedInstanceState);
}

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);

    // instantiate loading task
    mLoadMessagesTask = new LoadMessagesTask();

    // instantiate list of messages
    mMessagesList = new ArrayList<Message>();
    mAdapter = new MessageArrayAdapter(getActivity(), mMessagesList);
    setListAdapter(mAdapter);
}

@Override
public void onResume() {
    mListener.onMessageInitialisation();
    super.onResume();
}

public void onListItemClick(ListView l, View v, int position, long id) {
    Message selectedMessage = (Message) getListAdapter().getItem(position);
    mListener.onMessageSelected(selectedMessage);
    super.onListItemClick(l, v, position, id);
}

/* public methods to load messages from host acitivity, etc... */
}

Solution

La solution bête consiste à enregistrer les fragments à l'intérieur de onSaveInstanceState (de l'activité de l'hôte) avec putFragment et de les placer dans onCreate via getFragment. Mais j'ai toujours le sentiment étrange que les choses ne devraient pas fonctionner comme ça ... Voir le code ci-dessous:

    @Override
protected void onSaveInstanceState(Bundle outState) {

    super.onSaveInstanceState(outState);
    getSupportFragmentManager()
            .putFragment(outState, MessagesFragment.class.getName(), mMessagesFragment);
}

protected void onCreate(Bundle savedInstanceState) {
    Logger.d("Dash onCreate");
    super.onCreate(savedInstanceState);

    ...
    // create fragments to use
    if (savedInstanceState != null) {
        mMessagesFragment = (MessagesFragment) getSupportFragmentManager().getFragment(
                savedInstanceState, MessagesFragment.class.getName());
                StreamsFragment.class.getName());
    }
    if (mMessagesFragment == null)
        mMessagesFragment = new MessagesFragment();
    ...
}

459
2017-10-31 09:18


origine


Réponses:


Quand le FragmentPagerAdapter ajoute un fragment au FragmentManager, il utilise une balise spéciale en fonction de la position particulière du fragment. FragmentPagerAdapter.getItem(int position) est seulement appelé quand un fragment pour cette position n'existe pas. Après la rotation, Android remarquera qu’il a déjà créé / enregistré un fragment pour cette position particulière et qu’il tente simplement de se reconnecter avec FragmentManager.findFragmentByTag(), au lieu d'en créer un nouveau. Tout cela est gratuit lorsque vous utilisez le FragmentPagerAdapter et c'est pourquoi il est habituel d'avoir votre code d'initialisation de fragment à l'intérieur du getItem(int) méthode.

Même si nous n'utilisions pas de FragmentPagerAdapter, ce n'est pas une bonne idée de créer un nouveau fragment à chaque fois Activity.onCreate(Bundle). Comme vous l'avez remarqué, lorsqu'un fragment est ajouté au FragmentManager, il sera recréé pour vous après la rotation et il n'est pas nécessaire de le rajouter. Cela est une cause fréquente d'erreurs lorsque vous travaillez avec des fragments.

Une approche habituelle lorsque vous travaillez avec des fragments est la suivante:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    ...

    CustomFragment fragment;
    if (savedInstanceState != null) {
        fragment = (CustomFragment) getSupportFragmentManager().findFragmentByTag("customtag");
    } else {
        fragment = new CustomFragment();
        getSupportFragmentManager().beginTransaction().add(R.id.container, fragment, "customtag").commit(); 
    }

    ...

}

Lorsque vous utilisez un FragmentPagerAdapter, nous abandonnons la gestion des fragments à l'adaptateur et n'avons pas à effectuer les étapes ci-dessus. Par défaut, il ne précharge qu'un fragment devant et derrière la position actuelle (bien qu'il ne les détruise pas à moins que vous n'utilisiez FragmentStatePagerAdapter). Ceci est contrôlé par ViewPager.setOffscreenPageLimit (int). De ce fait, il n'est pas garanti que l'appel direct aux méthodes sur les fragments en dehors de l'adaptateur soit valide, car il se peut même qu'elles ne soient pas actives.

Pour couper court une histoire courte, votre solution à utiliser putFragment être capable d'obtenir une référence par la suite n'est pas si fou, et pas si différent de la façon normale d'utiliser des fragments de toute façon (ci-dessus). Il est difficile d'obtenir une référence autrement, car le fragment est ajouté par l'adaptateur, et pas vous personnellement. Assurez-vous simplement que le offscreenPageLimit est assez élevé pour charger vos fragments désirés à tout moment, puisque vous comptez sur sa présence. Cela contourne les capacités de chargement paresseux du ViewPager, mais semble être ce que vous désirez pour votre application.

Une autre approche consiste à remplacer FragmentPageAdapter.instantiateItem(View, int) et enregistrer une référence au fragment renvoyé par le super-appel avant de le renvoyer (il a la logique de trouver le fragment, s'il est déjà présent).

Pour une image plus complète, jetez un coup d’œil à la source de FragmentPagerAdapter (court et ViewPager (longue).


425
2018-03-10 13:00



Je veux offrir une solution qui se développe sur antonytde réponse merveilleuse et mention du dépassement FragmentPageAdapter.instantiateItem(View, int) enregistrer les références à créer Fragments donc vous pouvez y travailler plus tard. Cela devrait également fonctionner avec FragmentStatePagerAdapter; voir les notes pour plus de détails.


Voici un exemple simple de comment obtenir une référence à la Fragments retourné par FragmentPagerAdapter qui ne repose pas sur le interne tags mis sur le Fragments. La clé est de remplacer instantiateItem() et enregistrer des références là-bas au lieu 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 au lieu des variables membres de la classe / références à Fragments vous pouvez également saisir le tags fixé 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 l’imitation interne tag fixé par FragmentPagerAdapter et utilise à la place les API appropriées pour les récupérer. De cette façon, même si le tag changements dans les versions futures du SupportLibrary vous serez toujours en sécurité.


N'oublie pas que selon la conception de votre Activity, la Fragments vous essayez de travailler peut-être ou ne pas exister encore, donc vous devez en tenir compte en faisant null vérifie 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 à votre Fragments parce que vous pourriez en avoir beaucoup et que les références matérielles les garderaient inutilement en mémoire. Au lieu de sauvegarder le Fragment références en WeakReference les variables au lieu 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...
}

33
2018-03-26 20:22



J'ai trouvé une autre solution relativement facile pour votre question.

Comme vous pouvez le voir Code source FragmentPagerAdapter, les fragments gérés par FragmentPagerAdapter stocker dans le FragmentManager sous l'étiquette générée en utilisant:

String tag="android:switcher:" + viewId + ":" + index;

le viewId est le container.getId(), la containerest ton ViewPager exemple. le index est la position du fragment. Vous pouvez donc enregistrer l’identifiant de l’objet sur le outState:

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putInt("viewpagerid" , mViewPager.getId() );
}

@Override
    protected void onCreate(Bundle savedInstanceState) {
    setContentView(R.layout.activity_main);
    if (savedInstanceState != null)
        viewpagerid=savedInstanceState.getInt("viewpagerid", -1 );  

    MyFragmentPagerAdapter titleAdapter = new MyFragmentPagerAdapter (getSupportFragmentManager() , this);        
    mViewPager = (ViewPager) findViewById(R.id.pager);
    if (viewpagerid != -1 ){
        mViewPager.setId(viewpagerid);
    }else{
        viewpagerid=mViewPager.getId();
    }
    mViewPager.setAdapter(titleAdapter);

Si vous voulez communiquer avec ce fragment, vous pouvez obtenir FragmentManager, tel que:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")

16
2018-03-03 04:17



Je veux offrir une solution de rechange pour un cas peut-être un peu différent, puisque beaucoup de mes recherches de réponses m'ont conduit à ce fil.

Mon cas - Je crée / ajoute des pages de manière dynamique et les glisse dans un ViewPager, mais lorsque je suis pivoté (onConfigurationChange), je me retrouve avec une nouvelle page car bien sûr, OnCreate est appelée à nouveau. Mais je veux garder une référence à toutes les pages qui ont été créées avant la rotation.

Problème - Je n'ai pas d'identifiant unique pour chaque fragment que je crée. La seule façon de faire référence était donc de stocker les références dans un tableau à restaurer après le changement de rotation / configuration.

solution de contournement - Le concept clé consistait à faire en sorte que l'activité (qui affiche les fragments) gère également le tableau de références aux fragments existants, car cette activité peut utiliser des ensembles dans onSaveInstanceState.

public class MainActivity extends FragmentActivity

Donc, dans cette activité, je déclare un membre privé pour suivre les pages ouvertes

private List<Fragment> retainedPages = new ArrayList<Fragment>();

Ceci est mis à jour à chaque fois que onSaveInstanceState est appelé et restauré dans onCreate

@Override
protected void onSaveInstanceState(Bundle outState) {
    retainedPages = _adapter.exportList();
    outState.putSerializable("retainedPages", (Serializable) retainedPages);
    super.onSaveInstanceState(outState);
}

... donc une fois qu'il est stocké, il peut être récupéré ...

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    if (savedInstanceState != null) {
        retainedPages = (List<Fragment>) savedInstanceState.getSerializable("retainedPages");
    }
    _mViewPager = (CustomViewPager) findViewById(R.id.viewPager);
    _adapter = new ViewPagerAdapter(getApplicationContext(), getSupportFragmentManager());
    if (retainedPages.size() > 0) {
        _adapter.importList(retainedPages);
    }
    _mViewPager.setAdapter(_adapter);
    _mViewPager.setCurrentItem(_adapter.getCount()-1);
}

Ce sont les changements nécessaires à l'activité principale, et donc j'avais besoin des membres et des méthodes dans mon FragmentPagerAdapter pour que cela fonctionne, donc dans

public class ViewPagerAdapter extends FragmentPagerAdapter

une construction identique (comme indiqué ci-dessus dans MainActivity)

private List<Fragment> _pages = new ArrayList<Fragment>();

et cette synchronisation (comme utilisé ci-dessus dans onSaveInstanceState) est pris en charge spécifiquement par les méthodes

public List<Fragment> exportList() {
    return _pages;
}

public void importList(List<Fragment> savedPages) {
    _pages = savedPages;
}

Et puis enfin, dans la classe des fragments

public class CustomFragment extends Fragment

pour que tout cela fonctionne, il y avait deux changements, d'abord

public class CustomFragment extends Fragment implements Serializable

puis en ajoutant ceci à onCreate afin que les fragments ne soient pas détruits

setRetainInstance(true);

Je suis toujours en train de me concentrer sur le cycle de vie de Fragments and Android. Il est donc important de souligner que cette méthode peut entraîner des redondances / inefficacités. Mais cela fonctionne pour moi et j'espère que cela pourrait être utile pour d'autres cas similaires à la mienne.


15
2017-12-04 04:12



Ma solution est très grossière mais fonctionne: étant mes fragments créés dynamiquement à partir de données conservées, je supprime simplement tout fragment de la PageAdapter avant d'appeler super.onSaveInstanceState() puis recréez-les lors de la création d'activité:

@Override
protected void onSaveInstanceState(Bundle outState) {
    outState.putInt("viewpagerpos", mViewPager.getCurrentItem() );
    mSectionsPagerAdapter.removeAllfragments();
    super.onSaveInstanceState(outState);
}

Vous ne pouvez pas les supprimer onDestroy(), sinon vous obtenez cette exception:

java.lang.IllegalStateException: Impossible d'effectuer cette action après onSaveInstanceState

Voici le code dans l'adaptateur de page:

public void removeAllfragments()
{
    if ( mFragmentList != null ) {
        for ( Fragment fragment : mFragmentList ) {
            mFm.beginTransaction().remove(fragment).commit();
        }
        mFragmentList.clear();
        notifyDataSetChanged();
    }
}

Je ne sauvegarde que la page en cours et la restaure onCreate(), après que les fragments ont été créés.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  

8
2018-04-17 09:51



Qu'est-ce que c'est BasePagerAdapter? Vous devez utiliser l'un des adaptateurs de pager standard - soit FragmentPagerAdapter ou FragmentStatePagerAdapter, selon que vous voulez des fragments qui ne sont plus nécessaires par le ViewPager soit être conservé autour (le premier) ou avoir leur état sauvegardé (le dernier) et recréé si nécessaire à nouveau.

Exemple de code pour l'utilisation ViewPager peut être trouvé ici

Il est vrai que la gestion des fragments dans un pager de vue à travers les instances d'activité est un peu compliquée, car le FragmentManagerdans le cadre prend soin de sauver l'état et de restaurer tous les fragments actifs que le pager a fait. Tout cela signifie vraiment que l'adaptateur lors de l'initialisation doit s'assurer qu'il se reconnecte avec les fragments restaurés. Vous pouvez regarder le code pour FragmentPagerAdapter ou FragmentStatePagerAdapter pour voir comment cela est fait.


4
2017-11-04 07:07



Si quelqu'un rencontre des problèmes avec son FragmentStatePagerAdapter ne restaure pas correctement l'état de ses fragments ... c.-à-d. ... les nouveaux fragments sont créés par le FragmentStatePagerAdapter au lieu de les restaurer depuis l'état ...

Assurez-vous d'appeler ViewPager.setOffscreenPageLimit() AVANT d'appeler ViewPager.setAdapeter(fragmentStatePagerAdapter)

En appelant ViewPager.setOffscreenPageLimit()... le ViewPager va immédiatement regarder à son adaptateur et essayer de récupérer ses fragments. Cela peut se produire avant que ViewPager ait une chance de restaurer les fragments à partir de savedInstanceState (créant ainsi de nouveaux fragments qui ne peuvent pas être réinitialisés à partir de SavedInstanceState car ils sont nouveaux).


2
2018-02-05 01:22