511 votes

ViewPager et fragments — quel est le bon chemin pour stocker fragment de l'état?

Fragments semblent être très agréable pour la séparation de la logique de l'INTERFACE utilisateur en plusieurs modules. Mais avec ViewPager de son cycle de vie est encore brumeux pour moi. Donc Gourou pensées sont mal nécessaire!

Modifier

Voir dumb solution ci-dessous ;-)

Portée

L'activité principale dispose d'un ViewPager avec des fragments. Ces fragments pourraient mettre en œuvre un peu la même logique pour les autres (submain) des activités, de sorte que les fragments de données est rempli à l'aide d'une interface de rappel à l'intérieur de l'activité. Et tout fonctionne bien sur la première de lancement, mais!...

Problème

Lorsque l'activité est recréé (par exemple sur le changement d'orientation) afin de faire l' ViewPagers'fragments. Le code (vous trouverez ci-après) dit que chaque fois que l'activité est créée que j'essaie de créer un nouveau ViewPager des fragments de l'adaptateur de même que des fragments (c'est peut-être le problème) mais FragmentManager a déjà tous ces fragments stockés quelque part (où?) et commence le mécanisme de loisirs pour les personnes. De sorte que le mécanisme de loisirs appelle le "vieux" fragment de la onAttach, onCreateView, etc. avec mon interface de rappel d'appel pour lancer des données sur l'Activité de la méthode mise en œuvre. Mais cette méthode de points pour le nouveau fragment qui est créé par l'Activité de la méthode onCreate.

Question

Peut-être que j'utilise mal les motifs, mais même Android 3 Pro livre n'a pas beaucoup à ce sujet. Donc, svp, donnez-moi une pierre deux coups et de montrer comment faire de la bonne façon. Merci beaucoup!

Code

L'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

Le muet solution est de sauver les fragments à l'intérieur de onSaveInstanceState (de l'Activité de l'hôte) avec putFragment et de les obtenir à l'intérieur de onCreate via getFragment. Mais j'ai toujours un sentiment étrange que les choses ne fonctionnent pas comme ça... Voir 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();
    ...
}

457voto

antonyt Points 10649

Lorsque l' FragmentPagerAdapter ajoute un fragment de la FragmentManager, il utilise une balise spéciale basée sur la position particulière que le fragment sera placé. FragmentPagerAdapter.getItem(int position) n'est appelée lorsqu'un fragment de cette position n'existe pas. Après la rotation, Android remarquerez qu'elle a déjà créé/enregistré un fragment de cette position particulière et donc il essaie simplement de renouer avec il avec FragmentManager.findFragmentByTag(), au lieu d'en créer un nouveau. Tout cela est fourni gratuitement lors de l'utilisation de l' FragmentPagerAdapter et c'est pourquoi il est habituel d'avoir votre fragment de code d'initialisation à l'intérieur de l' getItem(int) méthode.

Même si nous n'étions pas à l'aide d'un FragmentPagerAdapter, il n'est pas une bonne idée de créer un nouveau fragment de tous les temps, en Activity.onCreate(Bundle). Comme vous l'avez remarqué, lorsqu'un fragment est ajouté à la 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 lorsque l'on travaille avec des fragments.

Une approche habituelle lorsque l'on travaille avec les fragments, c'est ceci:

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

    ...

}

Lors de l'utilisation d'un FragmentPagerAdapter, nous renoncer fragment de gestion de la carte, et n'ont pas à effectuer les étapes ci-dessus. Par défaut, il suffira de le précharger un Fragment devant et derrière la position actuelle (bien qu'il ne les détruisez pas, sauf si vous utilisez FragmentStatePagerAdapter). Ceci est contrôlé par ViewPager.setOffscreenPageLimit(int). De ce fait, directement de l'appel de méthodes sur les fragments à l'extérieur de l'adaptateur n'est pas garanti pour être valide, car ils ne peuvent pas encore être en vie.

Pour couper une longue histoire courte, votre solution pour utiliser putFragment à être en mesure d'obtenir une référence par la suite n'est pas si fou, et pas tellement à la différence de la voie normale de l'utilisation de fragments de toute façon (ci-dessus). Il est difficile d'obtenir une référence sinon parce que le fragment est ajouté par la carte, et ne vous identifient pas personnellement. Assurez-vous que l' offscreenPageLimit est assez élevé pour charger votre choix fragments à tous les temps, puisque vous compter sur elle. Cela contourne paresseux des capacités de chargement de la ViewPager, mais semble être ce que vous désirez pour votre application.

Une autre approche consiste à substituer FragmentPageAdapter.instantiateItem(View, int) et enregistrer une référence à la fragment retourné à partir de la super appeler avant de la retourner (il a de la logique pour trouver le fragment, s'il est déjà présent).

Pour une image plus complète, jetez un oeil à certains de la source de FragmentPagerAdapter (court) et ViewPager (long).

18voto

user2110066 Points 101

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

Comme vous pouvez le voir à partir de la FragmentPagerAdapter code source, les fragments géré par FragmentPagerAdapter magasin dans le FragmentManager sous la balise générée à l'aide de:

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

Le viewId est le conteneur.getId(), le conteneur est votre ViewPager instance. L'indice est la position du fragment. Par conséquent, vous pouvez enregistrer l'id de l'objet à la 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 si de FragmentManager, tels que:

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

16voto

FSY Points 243

Je veux offrir une solution alternative pour peut-être un cas un peu différent, puisque nombre de mes recherches pour les réponses gardé m'a conduit à ce fil.

Mon cas - Je suis de la création/ajout des pages dynamiquement et en les faisant glisser dans un ViewPager, mais lors de la rotation (onConfigurationChange), je me retrouve avec une nouvelle page parce que, bien sûr OnCreate est appelé à nouveau. Mais je veux garder de référence pour toutes les pages qui ont été créées avant la rotation.

Problème - Je n'ai pas d'identifiants uniques pour chaque fragment que j'ai créer, de sorte que le seul moyen de faire référence à un moyen de stocker les références dans un Tableau pour être restaurée après la rotation/changement de configuration.

Solution de contournement - Le concept clé est d'avoir de l'Activité (qui affiche les Fragments) aussi gérer le tableau de références à des Fragments, étant donné que cette activité peut utiliser des ensembles onSaveInstanceState

public class MainActivity extends FragmentActivity

Dans cette Activité, je déclare un membre privé pour suivre les pages ouvertes

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

Il 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 enregistré, 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);
}

Ces ont été les changements nécessaires à l'activité principale, et j'ai donc besoin de membres et de méthodes à l'intérieur de mon FragmentPagerAdapter pour que cela fonctionne, donc à l'intérieur de

public class ViewPagerAdapter extends FragmentPagerAdapter

un nombre identique de construire (comme indiqué ci-dessus dans MainActivity )

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

et cette synchronisation (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 le fragment de classe

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

et puis en ajoutant ceci à onCreate si les Fragments ne sont pas détruits

setRetainInstance(true);

Je suis toujours en train d'envelopper ma tête autour de Fragments et Android cycle de vie, afin de souligner ici il peut y avoir des licenciements/inefficacité de cette méthode. Mais cela fonctionne pour moi et, je l'espère, pourrait être utile pour d'autres personnes avec des cas similaires à la mienne.

8voto

Sir John Points 21

Ma solution est très impoli, mais fonctionne: étant mon fragments créés dynamiquement à partir des données conservées, j'ai simplement enlever tous les fragments de la PageAdapter AVANT d'appeler super.onSaveInstanceState() puis de les recréer sur la création d'activités:

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

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

java.lang.IllegalStateException: vous ne Pouvez pas effectuer cette action après onSaveInstanceState

Voici le code dans la page carte:

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

J'ai seulement enregistrer la page en cours et de le restaurer dans onCreate(), d'après les fragments ont été créés.

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

4voto

hackbod Points 55292

Qu'est ce que BasePagerAdapter? Vous devez utiliser l'un de la norme pager cartes -- FragmentPagerAdapter ou FragmentStatePagerAdapter, selon si vous voulez des Fragments qui ne sont plus nécessaires par la ViewPager à être maintenu autour de (l'ancien) ou d'un état enregistré (le dernier) et re-créés si nécessaire de nouveau.

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

Il est vrai que la gestion de fragments en vue de radiomessagerie à travers les instances d'activité est un peu compliqué, parce que le FragmentManager dans le cadre prend soin de l'enregistrement de l'état et de la restauration de tout actif fragments que le pager a fait. Tout cela signifie, c'est que la carte lors de l'initialisation doit assurez-vous qu'il se reconnecte avec ce que l'fragments restaurés il y a des. Vous pouvez regarder le code pour FragmentPagerAdapter ou FragmentStatePagerAdapter pour voir comment c'est fait.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X