ViewPager et fragments - quelle est la bonne façon de stocker l'état d'un fragment?

492

Les fragments semblent très bien pour la séparation de la logique de l'interface utilisateur dans certains modules. Mais avec ViewPagerson cycle de vie est encore brumeux pour moi. Les pensées de Guru sont donc absolument nécessaires!

Éditer

Voir la solution stupide ci-dessous ;-)

Portée

L'activité principale a un ViewPageravec des fragments. Ces fragments pourraient implémenter une logique légèrement différente pour d'autres activités (sous-domaines), de sorte que les données des fragments sont remplies via une interface de rappel à l'intérieur de 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), les ViewPagerfragments de. Le code (vous trouverez ci-dessous) dit que chaque fois que l'activité est créée, j'essaie de créer un nouvel ViewPageradaptateur de fragments de la même manière que les fragments (c'est peut-être le problème) mais FragmentManager a déjà tous ces fragments stockés quelque part (où?) Et démarre le mécanisme de loisirs pour ceux-ci. Ainsi, le mécanisme de récréation appelle les "anciens" fragments onAttach, onCreateView, etc. 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 le livre Android 3 Pro n'a pas grand-chose à ce sujet. Alors, s'il vous plaît , donnez-moi un coup de poing et montrez 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 helper

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 stupide consiste à enregistrer les fragments à l'intérieur de onSaveInstanceState (de l'activité hôte) avec putFragment et à les obtenir à l'intérieur 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();
    ...
}
Oleksii Malovanyi
la source
1
Je me demande maintenant: dois-je utiliser une approche très différente ou essayer de sauvegarder des fragments de l'activité principale (Dashboard) via onSavedInstancestate pour les utiliser dans onCreate (). Existe-t-il un moyen approprié de sauvegarder ces fragments et de les récupérer à partir de l'ensemble dans onCreate? Ils ne semblent pas être colisables ...
Oleksii Malovanyi
2
2ème approche fonctionne - voir "Sulution". Mais cela semble être un vilain morceau de code, n'est-ce pas?
Oleksii Malovanyi
1
Pour l'effort de nettoyage de la balise Android (détails ici: meta.stackexchange.com/questions/100529/… ), cela vous dérangerait-il de publier votre solution comme réponse et de la marquer comme sélectionnée? De cette façon, cela ne s'affichera pas comme une question sans réponse :)
Alexander Lucas
1
ouais, pense que c'est OK. J'espérais mieux que le mien ...
Oleksii Malovanyi
1
La solution stupide fonctionne-t-elle même? Cela me donne une exception de pointeur nul ..
Zhen Liu

Réponses:

451

Lorsque le FragmentPagerAdapterajoute un fragment au FragmentManager, il utilise une balise spéciale basée sur la position particulière où le fragment sera placé. FragmentPagerAdapter.getItem(int position)n'est appelé que lorsqu'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 essaie simplement de se reconnecter avec lui FragmentManager.findFragmentByTag(), au lieu d'en créer un nouveau. Tout cela est gratuit lorsque vous utilisez le FragmentPagerAdapteret c'est pourquoi il est habituel d'avoir votre code d'initialisation de fragment à l'intérieur de la getItem(int)méthode.

Même si nous n'utilisions pas a 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 l'ajouter à nouveau. Cela est une cause fréquente d'erreurs lors de l'utilisation de 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 a FragmentPagerAdapter, nous abandonnons la gestion des fragments à l'adaptateur et nous n'avons pas à effectuer les étapes ci-dessus. Par défaut, il ne préchargera qu'un seul fragment devant et derrière la position actuelle (bien qu'il ne les détruise que si vous l'utilisez FragmentStatePagerAdapter). Ceci est contrôlé par ViewPager.setOffscreenPageLimit (int) . Pour cette raison, il n'est pas garanti que les méthodes d'appel direct sur les fragments en dehors de l'adaptateur soient valides, car elles peuvent même ne pas être actives.

Pour faire court, votre solution à utiliser putFragmentpour pouvoir obtenir une référence par la suite n'est pas si folle, et pas si différente de la façon habituelle d'utiliser des fragments de toute façon (ci-dessus). Sinon, il est difficile d'obtenir une référence car le fragment est ajouté par l'adaptateur, et non par vous personnellement. Assurez-vous simplement que la offscreenPageLimithauteur est suffisamment élevée pour charger vos fragments souhaités à tout moment, car 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 œil à la source de FragmentPagerAdapter (court) et ViewPager (long).

antonyt
la source
7
J'ai adoré la dernière partie. Avait un cache pour les fragments et a déplacé le mettre dans la logique de cache à l'intérieur du FragmentPageAdapter.instantiateItem(View, int). Enfin corrigé un bug de longue durée qui n'apparaissait que dans le changement de rotation / config et me rendait fou ...
Carlos Sobrinho
2
Cela a résolu un problème que je rencontrais lors du changement de rotation. C'est peut-être que je ne peux pas voir pour regarder, mais est-ce documenté? c'est à dire en utilisant la balise pour récupérer d'un état précédent? C'est peut-être une réponse évidente, mais je suis relativement nouveau dans le développement Android.
1
@ Antidépresseur industriel C'est l'identifiant du conteneur (par exemple un FrameLayout) auquel le fragment doit être ajouté.
antonyt
1
btw, pour moi c'était FragmentPageAdapter.instantiateItem(ViewGroup, int)plutôt que FragmentPageAdapter.instantiateItem(View, int).
Nom d'affichage
1
Btw savez-vous quelle (le cas échéant) méthode de cycle de vie de fragment est appelée lorsque le fragment est glissé hors de l'écran? Est-ce que c'est onDetach()quelque chose d'autre?
ygesher
36

Je veux offrir une solution qui développe antonytla merveilleuse réponse et mention de la substitution FragmentPageAdapter.instantiateItem(View, int)pour enregistrer les références à créées Fragmentsafin que vous puissiez 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 au Fragmentsretourné par FragmentPagerAdapterqui ne dépend pas de l' tagsensemble interne du Fragments. La clé est de remplacer instantiateItem()et d'enregistrer les références dedans au lieu de dedans 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 tagsdes variables / références de membre de classe à la, Fragmentsvous pouvez également récupérer l' tagsensemble de FragmentPagerAdapterla même manière: REMARQUE: cela ne s'applique pas FragmentStatePagerAdaptercar il ne se définit pas tagslors 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 de l' tagensemble interne par FragmentPagerAdapteret utilise à la place des API appropriées pour les récupérer. De cette façon, même si les tagchangements dans les futures versions du sont SupportLibrarytoujours en sécurité.


N'oubliez pas qu'en fonction de la conception de votre Activity, le projet sur Fragmentslequel vous essayez de travailler peut ou non exister, vous devez donc en tenir compte en effectuant des nullvérifications avant d'utiliser vos références.

De plus, si au lieu de cela vous travaillez FragmentStatePagerAdapter, vous ne voulez pas conserver de références matérielles à votre Fragmentscar vous pourriez en avoir plusieurs et les références matérielles les garderaient inutilement en mémoire. Enregistrez plutôt les Fragmentréférences dans des WeakReferencevariables au lieu de celles 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...
}
Tony Chan
la source
en cas de manque de mémoire, Android pourrait demander au FragmentManager de détruire les fragments inutilisés, non? Si j'ai raison, votre deuxième version fonctionnera, la première pourrait échouer. (Je ne sais pas si les versions Android actuelles le font, mais il semble que nous devons nous y attendre.)
Moritz Both
1
qu'en est-il de la création d'un FragmetPagerAdapterin onCreate of Activity à chaque rotation d'écran. Est-ce faux parce qu'il peut contourner la réutilisation de fragments déjà ajoutés dansFragmentPagerAdapter
Bahman
Le dépassement instantiateItem()est la voie à suivre; cela m'a aidé à gérer les rotations d'écran et à récupérer mes instances de fragment existantes une fois l'activité et l'adaptateur repris; Je me suis laissé des commentaires dans le code pour rappel: après rotation, getItem()n'est PAS invoqué; seule cette méthode instantiateItem()est appelée. La super implémentation pour instantiateItem()réellement rattacher les fragments après rotation (si nécessaire), au lieu d'instancier de nouvelles instances!
HelloImKevo
Je reçois un pointeur nul Fragment createdFragment = (Fragment) super.instantiateItem..dans la première solution.
AlexS
Après avoir parcouru toutes les différentes itérations en double / variante de ce problème, c'était la meilleure solution. (
Renvoi
18

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

Comme vous pouvez le voir dans le code source de FragmentPagerAdapter , les fragments gérés par FragmentPagerAdaptermagasin dans le FragmentManagersous la balise sont générés à l'aide de:

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

Le viewIdest le container.getId(), containerc'est votre ViewPagerinstance. C'est indexla position du fragment. Par conséquent, vous pouvez enregistrer l'ID d'objet dans 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 souhaitez communiquer avec ce fragment, vous pouvez obtenir des informations provenant de FragmentManager, telles que:

getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewpagerid + ":0")
user2110066
la source
21
hmmm Je ne pense pas que ce soit une bonne façon de procéder, car vous répondez à une convention de dénomination de balise interne qui ne restera en aucun cas la même pour toujours.
Dori
1
Pour ceux qui recherchent une solution qui ne repose pas sur l'interne tag, pensez à essayer ma réponse .
Tony Chan
16

Je veux offrir une solution alternative pour peut-être un cas légèrement différent, car beaucoup de mes recherches de réponses m'ont amené à ce fil.

Mon cas - je crée / ajoute des pages dynamiquement et les glisse dans un ViewPager, mais quand je tourne (onConfigurationChange), je me retrouve avec une nouvelle page car bien sûr OnCreate est appelé à 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'identifiants uniques pour chaque fragment que je crée, donc la seule façon de référencer était de stocker en quelque sorte les références dans un tableau à restaurer après le changement de rotation / configuration.

Solution - Le concept clé était 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 bundles 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 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ée ci-dessus dans onSaveInstanceState) est prise en charge spécifiquement par les méthodes

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

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

Et puis finalement, dans la classe des fragments

public class CustomFragment extends Fragment

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

public class CustomFragment extends Fragment implements Serializable

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

setRetainInstance(true);

Je suis toujours en train d'envelopper ma tête autour des fragments et du cycle de vie d'Android, donc la mise en garde ici est qu'il peut y avoir des redondances / inefficacités dans cette méthode. Mais cela fonctionne pour moi et j'espère que cela pourrait être utile pour d'autres avec des cas similaires au mien.

Frank Yin
la source
2
+1 pour setRetainInstance (true) - la magie noire que je cherchais!
déclinaison
C'était encore plus facile en utilisant FragmentStatePagerAdapter (v13). Quelle affaire pour vous avec la restauration et la libération de l'état.
The Butcher
Description claire et très belle solution. Je vous remercie!
Bruno Bieri
8

Ma solution est très grossière mais fonctionne: étant mes fragments créés dynamiquement à partir des données conservées, je supprime simplement tous les fragments de PageAdapteravant d'appeler super.onSaveInstanceState()puis les recrée 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 sauvegarde uniquement la page actuelle et la restaure onCreate()après la création des fragments.

if (savedInstanceState != null)
    mViewPager.setCurrentItem( savedInstanceState.getInt("viewpagerpos", 0 ) );  
Sir John
la source
Je ne sais pas pourquoi, mais notifyDataSetChanged (); provoque le crash de l'application
Malachiasz
4

Qu'est-ce que c'est BasePagerAdapter? Vous devez utiliser l'un des adaptateurs de téléavertisseur standard - soitFragmentPagerAdapter ou FragmentStatePagerAdapter, selon que vous souhaitez que les fragments qui ne sont plus nécessaires ViewPagerau soient conservés (le premier) ou que leur état soit sauvegardé (le dernier) et recréé si nécessaire à nouveau.

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

Il est vrai que la gestion des fragments dans un pager de vue entre les instances d'activité est un peu compliquée, car le FragmentManagerdans le cadre prend soin de sauvegarder l'état et de restaurer tous les fragments actifs que le pager a créés. 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 consulter le code FragmentPagerAdapterou FragmentStatePagerAdapterpour voir comment cela se fait.

hackbod
la source
1
Le code BasePagerAdapter est disponible dans ma question. Comme on peut le voir, il étend simplement FragmentPagerAdapter dans le but d'implémenter TitleProvider . Donc, tout fonctionne déjà avec l'approche suggérée par les développeurs Android.
Oleksii Malovanyi
Je n'étais pas au courant de "FragmentStatePagerAdapter". Tu m'as sauvé littéralement des heures. Merci.
Knossos
2

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

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

Lors de l'appel ViewPager.setOffscreenPageLimit()... le ViewPager regardera immédiatement son adaptateur et essaiera d'obtenir ses fragments. Cela peut se produire avant que le ViewPager n'ait la possibilité 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).

dell116
la source
0

J'ai trouvé cette solution simple et élégante. Il suppose que l'activité est responsable de la création des fragments et que l'adaptateur les sert uniquement.

Ceci est le code de l'adaptateur (rien de bizarre ici, à part le fait qu'il mFragmentss'agit d'une liste de fragments maintenue par l'activité)

class MyFragmentPagerAdapter extends FragmentStatePagerAdapter {

    public MyFragmentPagerAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        return mFragments.get(position);
    }

    @Override
    public int getCount() {
        return mFragments.size();
    }

    @Override
    public int getItemPosition(Object object) {
        return POSITION_NONE;
    }

    @Override
    public CharSequence getPageTitle(int position) {
        TabFragment fragment = (TabFragment)mFragments.get(position);
        return fragment.getTitle();
    }
} 

Tout le problème de ce thread est d'obtenir une référence des "anciens" fragments, donc j'utilise ce code dans onCreate de l'activité.

    if (savedInstanceState!=null) {
        if (getSupportFragmentManager().getFragments()!=null) {
            for (Fragment fragment : getSupportFragmentManager().getFragments()) {
                mFragments.add(fragment);
            }
        }
    }

Bien sûr, vous pouvez affiner ce code si nécessaire, par exemple en vous assurant que les fragments sont des instances d'une classe particulière.

Merlevede
la source
0

Pour obtenir les fragments après un changement d'orientation, vous devez utiliser le .getTag ().

    getSupportFragmentManager().findFragmentByTag("android:switcher:" + viewPagerId + ":" + positionOfItemInViewPager)

Pour un peu plus de manipulation, j'ai écrit ma propre ArrayList pour mon PageAdapter pour obtenir le fragment par viewPagerId et FragmentClass à n'importe quelle position:

public class MyPageAdapter extends FragmentPagerAdapter implements Serializable {
private final String logTAG = MyPageAdapter.class.getName() + ".";

private ArrayList<MyPageBuilder> fragmentPages;

public MyPageAdapter(FragmentManager fm, ArrayList<MyPageBuilder> fragments) {
    super(fm);
    fragmentPages = fragments;
}

@Override
public Fragment getItem(int position) {
    return this.fragmentPages.get(position).getFragment();
}

@Override
public CharSequence getPageTitle(int position) {
    return this.fragmentPages.get(position).getPageTitle();
}

@Override
public int getCount() {
    return this.fragmentPages.size();
}


public int getItemPosition(Object object) {
    //benötigt, damit bei notifyDataSetChanged alle Fragemnts refrehsed werden

    Log.d(logTAG, object.getClass().getName());
    return POSITION_NONE;
}

public Fragment getFragment(int position) {
    return getItem(position);
}

public String getTag(int position, int viewPagerId) {
    //getSupportFragmentManager().findFragmentByTag("android:switcher:" + R.id.shares_detail_activity_viewpager + ":" + myViewPager.getCurrentItem())

    return "android:switcher:" + viewPagerId + ":" + position;
}

public MyPageBuilder getPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
    return new MyPageBuilder(pageTitle, icon, selectedIcon, frag);
}


public static class MyPageBuilder {

    private Fragment fragment;

    public Fragment getFragment() {
        return fragment;
    }

    public void setFragment(Fragment fragment) {
        this.fragment = fragment;
    }

    private String pageTitle;

    public String getPageTitle() {
        return pageTitle;
    }

    public void setPageTitle(String pageTitle) {
        this.pageTitle = pageTitle;
    }

    private int icon;

    public int getIconUnselected() {
        return icon;
    }

    public void setIconUnselected(int iconUnselected) {
        this.icon = iconUnselected;
    }

    private int iconSelected;

    public int getIconSelected() {
        return iconSelected;
    }

    public void setIconSelected(int iconSelected) {
        this.iconSelected = iconSelected;
    }

    public MyPageBuilder(String pageTitle, int icon, int selectedIcon, Fragment frag) {
        this.pageTitle = pageTitle;
        this.icon = icon;
        this.iconSelected = selectedIcon;
        this.fragment = frag;
    }
}

public static class MyPageArrayList extends ArrayList<MyPageBuilder> {
    private final String logTAG = MyPageArrayList.class.getName() + ".";

    public MyPageBuilder get(Class cls) {
        // Fragment über FragmentClass holen
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return super.get(indexOf(item));
            }
        }
        return null;
    }

    public String getTag(int viewPagerId, Class cls) {
        // Tag des Fragment unabhängig vom State z.B. nach bei Orientation change
        for (MyPageBuilder item : this) {
            if (item.fragment.getClass().getName().equalsIgnoreCase(cls.getName())) {
                return "android:switcher:" + viewPagerId + ":" + indexOf(item);
            }
        }
        return null;
    }
}

Il suffit donc de créer un MyPageArrayList avec les fragments:

    myFragPages = new MyPageAdapter.MyPageArrayList();

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_data_frag),
            R.drawable.ic_sd_storage_24dp,
            R.drawable.ic_sd_storage_selected_24dp,
            new WidgetDataFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_color_frag),
            R.drawable.ic_color_24dp,
            R.drawable.ic_color_selected_24dp,
            new WidgetColorFrag()));

    myFragPages.add(new MyPageAdapter.MyPageBuilder(
            getString(R.string.widget_config_textsize_frag),
            R.drawable.ic_settings_widget_24dp,
            R.drawable.ic_settings_selected_24dp,
            new WidgetTextSizeFrag()));

et ajoutez-les au viewPager:

    mAdapter = new MyPageAdapter(getSupportFragmentManager(), myFragPages);
    myViewPager.setAdapter(mAdapter);

après cela, vous pouvez obtenir après l'orientation changer le bon fragment en utilisant sa classe:

        WidgetDataFrag dataFragment = (WidgetDataFrag) getSupportFragmentManager()
            .findFragmentByTag(myFragPages.getTag(myViewPager.getId(), WidgetDataFrag.class));
Profiteur
la source
-30

ajouter:

   @SuppressLint("ValidFragment")

avant votre classe.

ça ne marche pas faire quelque chose comme ça:

@SuppressLint({ "ValidFragment", "HandlerLeak" })
andro_stackoverflow
la source