Les fragments semblent très bien 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 de Guru sont donc absolument nécessaires!
Éditer
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-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 ViewPager
fragments de. Le code (vous trouverez ci-dessous) dit que chaque fois que l'activité est créée, j'essaie de créer un nouvel ViewPager
adaptateur 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();
...
}
la source
Réponses:
Lorsque le
FragmentPagerAdapter
ajoute 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 luiFragmentManager.findFragmentByTag()
, au lieu d'en créer un nouveau. Tout cela est gratuit lorsque vous utilisez leFragmentPagerAdapter
et c'est pourquoi il est habituel d'avoir votre code d'initialisation de fragment à l'intérieur de lagetItem(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 foisActivity.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:
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'utilisezFragmentStatePagerAdapter
). 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
putFragment
pour 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 laoffscreenPageLimit
hauteur 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).
la source
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 ...FragmentPageAdapter.instantiateItem(ViewGroup, int)
plutôt queFragmentPageAdapter.instantiateItem(View, int)
.onDetach()
quelque chose d'autre?Je veux offrir une solution qui développe
antonyt
la merveilleuse réponse et mention de la substitutionFragmentPageAdapter.instantiateItem(View, int)
pour enregistrer les références à crééesFragments
afin que vous puissiez y travailler plus tard. Cela devrait également fonctionner avecFragmentStatePagerAdapter
; voir les notes pour plus de détails.Voici un exemple simple de comment obtenir une référence au
Fragments
retourné parFragmentPagerAdapter
qui ne dépend pas de l'tags
ensemble interne duFragments
. La clé est de remplacerinstantiateItem()
et d'enregistrer les références dedans au lieu de dedansgetItem()
.ou si vous préférez travailler avec
tags
des variables / références de membre de classe à la,Fragments
vous pouvez également récupérer l'tags
ensemble deFragmentPagerAdapter
la même manière: REMARQUE: cela ne s'applique pasFragmentStatePagerAdapter
car il ne se définit pastags
lors de la création de sonFragments
.Notez que cette méthode ne repose PAS sur l'imitation de l'
tag
ensemble interne parFragmentPagerAdapter
et utilise à la place des API appropriées pour les récupérer. De cette façon, même si lestag
changements dans les futures versions du sontSupportLibrary
toujours en sécurité.N'oubliez pas qu'en fonction de la conception de votre
Activity
, le projet surFragments
lequel vous essayez de travailler peut ou non exister, vous devez donc en tenir compte en effectuant desnull
vé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 à votreFragments
car vous pourriez en avoir plusieurs et les références matérielles les garderaient inutilement en mémoire. Enregistrez plutôt lesFragment
références dans desWeakReference
variables au lieu de celles standard. Comme ça:la source
FragmetPagerAdapter
in 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
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éthodeinstantiateItem()
est appelée. La super implémentation pourinstantiateItem()
réellement rattacher les fragments après rotation (si nécessaire), au lieu d'instancier de nouvelles instances!Fragment createdFragment = (Fragment) super.instantiateItem..
dans la première solution.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
FragmentPagerAdapter
magasin dans leFragmentManager
sous la balise sont générés à l'aide de:Le
viewId
est lecontainer.getId()
,container
c'est votreViewPager
instance. C'estindex
la position du fragment. Par conséquent, vous pouvez enregistrer l'ID d'objet dansoutState
:Si vous souhaitez communiquer avec ce fragment, vous pouvez obtenir des informations provenant de
FragmentManager
, telles que:la source
tag
, pensez à essayer ma réponse .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
Donc, dans cette activité, je déclare un membre privé pour suivre les pages ouvertes
Ceci est mis à jour chaque fois que onSaveInstanceState est appelé et restauré dans onCreate
... donc une fois stocké, il peut être récupéré ...
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
une construction identique (comme indiqué ci-dessus dans MainActivity)
et cette synchronisation (comme utilisée ci-dessus dans onSaveInstanceState) est prise en charge spécifiquement par les méthodes
Et puis finalement, dans la classe des fragments
pour que tout cela fonctionne, il y a eu deux changements, d'abord
puis en ajoutant ceci à onCreate pour que les fragments ne soient pas détruits
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.
la source
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
PageAdapter
avant d'appelersuper.onSaveInstanceState()
puis les recrée lors de la création d'activité:Vous ne pouvez pas les supprimer
onDestroy()
, sinon vous obtenez cette exception:java.lang.IllegalStateException:
Impossible d'effectuer cette action aprèsonSaveInstanceState
Voici le code dans l'adaptateur de page:
Je sauvegarde uniquement la page actuelle et la restaure
onCreate()
après la création des fragments.la source
Qu'est-ce que c'est
BasePagerAdapter
? Vous devez utiliser l'un des adaptateurs de téléavertisseur standard - soitFragmentPagerAdapter
ouFragmentStatePagerAdapter
, selon que vous souhaitez que les fragments qui ne sont plus nécessairesViewPager
au 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é iciIl est vrai que la gestion des fragments dans un pager de vue entre les instances d'activité est un peu compliquée, car le
FragmentManager
dans 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 codeFragmentPagerAdapter
ouFragmentStatePagerAdapter
pour voir comment cela se fait.la source
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).la source
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
mFragments
s'agit d'une liste de fragments maintenue par l'activité)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é.
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.
la source
Pour obtenir les fragments après un changement d'orientation, vous devez utiliser le .getTag ().
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:
Il suffit donc de créer un MyPageArrayList avec les fragments:
et ajoutez-les au viewPager:
après cela, vous pouvez obtenir après l'orientation changer le bon fragment en utilisant sa classe:
la source
ajouter:
avant votre classe.
ça ne marche pas faire quelque chose comme ça:
la source