534 votes

Comment sauvegarder correctement l'état d'instance des fragments dans la pile arrière ?

J'ai trouvé de nombreuses occurrences d'une question similaire sur SO mais malheureusement aucune réponse ne répond à mes exigences.

J'ai des mises en page différentes pour le portrait et le paysage et j'utilise la pile arrière, ce qui m'empêche d'utiliser setRetainState() et des astuces utilisant les routines de changement de configuration.

J'affiche certaines informations à l'utilisateur dans des TextView, qui ne sont pas enregistrées dans le gestionnaire par défaut. Lorsque j'ai écrit mon application en utilisant uniquement des Activities, ce qui suit a bien fonctionné :

TextView vstup;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.whatever);
    vstup = (TextView)findViewById(R.id.whatever);
    /* (...) */
}

@Override
public void onSaveInstanceState(Bundle state) {
    super.onSaveInstanceState(state);
    state.putCharSequence(App.VSTUP, vstup.getText());
}

@Override
public void onRestoreInstanceState(Bundle state) {
    super.onRestoreInstanceState(state);
    vstup.setText(state.getCharSequence(App.VSTUP));
}

Avec des Fragments, cela fonctionne uniquement dans des situations très spécifiques. En particulier, ce qui se casse horriblement, c'est le remplacement d'un fragment, sa mise dans la pile arrière, puis la rotation de l'écran alors que le nouveau fragment est affiché. D'après ce que j'ai compris, l'ancien fragment ne reçoit pas d'appel à onSaveInstanceState() lorsqu'il est remplacé mais reste d'une certaine manière lié à l'Activity et cette méthode est appelée plus tard lorsque sa View n'existe plus, donc chercher l'un de mes TextViews entraîne une NullPointerException.

J'ai également constaté que conserver la référence à mes TextViews n'est pas une bonne idée avec les Fragments, même si c'était correct avec les Activitys. Dans ce cas, onSaveInstanceState() enregistre effectivement l'état mais le problème réapparaît si je tourne l'écran deux fois lorsque le fragment est caché, car son onCreateView() n'est pas appelé dans la nouvelle instance.

J'ai pensé à enregistrer l'état dans onDestroyView() dans un élément de type Bundle (en fait, il s'agit de plusieurs données, pas seulement un TextView) et enregistrer celui-ci dans onSaveInstanceState() mais il y a d'autres inconvénients. Principalement, si le fragment est actuellement affiché, l'ordre d'appel des deux fonctions est inversé, donc je dois prendre en compte deux situations différentes. Il doit exister une solution plus propre et correcte !

1 votes

Voici un très bon exemple avec une explication détaillée également. emuneee.com/blog/2013/01/07/saving-fragment-states

1 votes

Je soutiens le lien emunee.com. Cela a résolu un problème d'interface utilisateur pour moi !

579voto

ThanhHH Points 613

Pour enregistrer correctement l'état de l'instance du Fragment, vous devez faire ce qui suit :

1. Dans le fragment, enregistrez l'état de l'instance en remplacant onSaveInstanceState() et restaurez-le dans onActivityCreated() :

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restaurer l'état du fragment ici
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Enregistrer l'état du fragment ici
    }

}

2. Et point important, dans l'activité, vous devez enregistrer l'instance du fragment dans onSaveInstanceState() et restaurer dans onCreate().

class MyActivity extends Activity {

    private MyFragment mMyFragment;

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            //Restaurer l'instance du fragment
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
            ...
        }
        ...
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Enregistrer l'instance du fragment
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}

8 votes

Cela a parfaitement fonctionné pour moi! Pas de contournements, pas de piratages, cela a simplement du sens de cette manière. Merci pour cela, des heures de recherche fructueuses. SaveInstanceState() de vos valeurs dans le fragment, puis enregistrez le fragment dans l'activité contenant le fragment, puis restaurez :)

14 votes

@wizurd mContent est un Fragment, il fait référence à l'instance du fragment actuel dans l'activité.

0 votes

Si vous avez des fragments qui peuvent être présents ou non (créés, détruits ou remplacés par des actions de l'utilisateur), vous pouvez dans le onCreate de l'Activité, if (savedInstanceState != null) faire ceci : if (savedInstanceState.containsKey(FRAG_TAG)) {frag1 = (FragOne) getFragmentManager().getFragment(savedInstanceState, FRAG_TAG);

92voto

Vašek Potoček Points 758

Ceci est une réponse très ancienne.

Je n'écris plus pour Android donc la fonctionnalité dans les versions récentes n'est pas garantie et il n'y aura pas de mises à jour à ce sujet.

Voici la méthode que j'utilise actuellement... c'est très compliqué mais au moins cela gère toutes les situations possibles. Au cas où quelqu'un serait intéressé.

public final class MyFragment extends Fragment {
    private TextView vstup;
    private Bundle savedState = null;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View v = inflater.inflate(R.layout.whatever, null);
        vstup = (TextView)v.findViewById(R.id.whatever);

        /* (...) */

        /* Si le Fragment a été détruit entre-temps (rotation de l'écran), nous devons d'abord récupérer savedState */
        /* Cependant, s'il ne l'a pas été, il reste dans l'instance de la dernière onDestroyView() et nous ne voulons pas l'écraser */
        if(savedInstanceState != null && savedState == null) {
            savedState = savedInstanceState.getBundle(App.STAV);
        }
        if(savedState != null) {
            vstup.setText(savedState.getCharSequence(App.VSTUP));
        }
        savedState = null;

        return v;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        savedState = saveState(); /* vstup est défini ici, c'est sûr */
        vstup = null;
    }

    private Bundle saveState() { /* appelé soit depuis onDestroyView() ou onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* Si onDestroyView() est appelé en premier, nous pouvons utiliser savedState précédemment sauvegardé mais nous ne pouvons plus appeler saveState() */
        /* Si onSaveInstanceState() est appelé en premier, nous n'avons pas savedState, donc nous devons appeler saveState() */
        /* => L'opérateur (?:) est inévitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

Autrement, il est toujours possible de conserver les données affichées dans des View passifs dans des variables et d'utiliser les View uniquement pour les afficher, en maintenant les deux éléments synchronisés. Cependant, je ne considère pas cela comme très propre.

72 votes

C'est la meilleure solution que j'ai trouvée jusqu'à présent mais il reste encore un problème (quelque peu exotique) : si vous avez deux fragments, A et B, où A est actuellement sur la pile arrière et B est visible, alors vous perdez l'état de A (celui invisible) si vous faites pivoter l'affichage deux fois. Le problème est que onCreateView() n'est pas appelée dans ce scénario, seulement onCreate(). Donc plus tard, dans onSaveInstanceState(), il n'y a pas de vues à sauvegarder l'état. Il faudrait stocker et ensuite sauvegarder l'état passé dans onCreate().

8 votes

@devconsole Je souhaiterais pouvoir vous donner 5 votes positifs pour ce commentaire! Cette rotation deux fois m'a tué pendant des jours.

0 votes

Merci pour la super réponse! J'ai cependant une question. Où est le meilleur endroit pour instancier un objet model (POJO) dans ce fragment?

65voto

Ricardo Points 1188

Sur la dernière bibliothèque de support, aucune des solutions discutées ici n'est plus nécessaire. Vous pouvez jouer avec les fragments de votre Activity comme vous le souhaitez en utilisant la FragmentTransaction. Assurez-vous simplement que vos fragments peuvent être identifiés soit par un id, soit par un tag.

Les fragments seront restaurés automatiquement tant que vous n'essayez pas de les recréer à chaque appel de onCreate(). Au lieu de cela, vous devriez vérifier si savedInstanceState n'est pas nul et retrouver les anciennes références aux fragments créés dans ce cas.

Voici un exemple :

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

    if (savedInstanceState == null) {
        myFragment = MyFragment.newInstance();
        getSupportFragmentManager()
                .beginTransaction()
                .add(R.id.my_container, myFragment, MY_FRAGMENT_TAG)
                .commit();
    } else {
        myFragment = (MyFragment) getSupportFragmentManager()
                .findFragmentByTag(MY_FRAGMENT_TAG);
    }
...
}

Cependant, notez qu'il y a actuellement un bug lors de la restauration de l'état caché d'un fragment. Si vous cachez des fragments dans votre activité, vous devrez dans ce cas restaurer cet état manuellement.

2 votes

Est-ce que cette correction est quelque chose que vous avez remarquée en utilisant la bibliothèque de support ou l'avez-vous lue quelque part? Y a-t-il plus d'informations que vous pourriez fournir à ce sujet? Merci!

1 votes

@Piovezan cela peut être en quelque sorte implicitement déduit à partir de la documentation. Par exemple, le [document beginTransaction()](http://developer.android.com/reference/android/app/FragmentManager.html#beginTransaction()) se lit comme suit : "Cela est dû au fait que le framework se charge de sauvegarder vos fragments actuels dans l'état (...)" . J'ai également codé mes applications avec ce comportement attendu depuis un certain temps maintenant.

1 votes

@Ricardo est-ce que cela s'applique si l'on utilise un ViewPager?

17voto

DroidT Points 544

Je veux juste donner la solution à laquelle je suis arrivé qui gère tous les cas présentés dans ce post que j'ai dérivés de Vasek et devconsole. Cette solution gère également le cas spécial lorsque le téléphone est tourné plusieurs fois alors que les fragments ne sont pas visibles.

Voici où je stocke le bundle pour une utilisation ultérieure car onCreate et onSaveInstanceState sont les seules fonctions appelées lorsque le fragment n'est pas visible

MonObjet monObjet;
private Bundle savedState = null;
private boolean createdStateInDestroyView;
private static final String SAVED_BUNDLE_TAG = "saved_bundle";

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState != null) {
        savedState = savedInstanceState.getBundle(SAVED_BUNDLE_TAG);
    }
}

Comme destroyView n'est pas appelé dans la situation de rotation spéciale, nous pouvons être certains que s'il crée l'état, nous devrions l'utiliser.

@Override
public void onDestroyView() {
    super.onDestroyView();
    savedState = saveState();
    createdStateInDestroyView = true;
    myObject = null;
}

Cette partie serait la même.

private Bundle saveState() { 
    Bundle state = new Bundle();
    state.putSerializable(SAVED_BUNDLE_TAG, myObject);
    return state;
}

Maintenant ici est la partie délicate. Dans ma méthode onActivityCreated, j'instancie la variable "myObject" mais la rotation se produit onActivity et onCreateView ne sont pas appelés. Par conséquent, monObjet sera nul dans cette situation lorsque l'orientation est modifiée plus d'une fois. Je contourne cela en réutilisant le même bundle qui a été enregistré dans onCreate en tant que bundle sortant.

@Override
public void onSaveInstanceState(Bundle outState) {

    if (myObject == null) {
        outState.putBundle(SAVED_BUNDLE_TAG, savedState);
    } else {
        outState.putBundle(SAVED_BUNDLE_TAG, createdStateInDestroyView ? savedState : saveState());
    }
    createdStateInDestroyView = false;
    super.onSaveInstanceState(outState);
}

Maintenant, partout où vous voulez restaurer l'état, utilisez simplement le bundle savedState

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}

0 votes

Pouvez-vous me dire... Qu'est-ce que "MyObject" ici ?

2 votes

Tout ce que vous voulez qu'il soit. C'est juste un exemple représentant quelque chose qui serait sauvegardé dans le bundle.

5voto

Noturno Points 121

Merci à DroidT, j'ai fait cela :

Je réalise que si le Fragment n'exécute pas onCreateView(), sa vue n'est pas instanciée. Donc, si le fragment sur la pile arrière n'a pas créé ses vues, je sauvegarde le dernier état enregistré, sinon je construis mon propre bundle avec les données que je veux sauvegarder/restaurer.

1) Etendez cette classe :

import android.os.Bundle;
import android.support.v4.app.Fragment;

public abstract class StatefulFragment extends Fragment {

    private Bundle savedState;
    private boolean saved;
    private static final String _FRAGMENT_STATE = "FRAGMENT_STATE";

    @Override
    public void onSaveInstanceState(Bundle state) {
        if (getView() == null) {
            state.putBundle(_FRAGMENT_STATE, savedState);
        } else {
            Bundle bundle = saved ? savedState : getStateToSave();

            state.putBundle(_FRAGMENT_STATE, bundle);
        }

        saved = false;

        super.onSaveInstanceState(state);
    }

    @Override
    public void onCreate(Bundle state) {
        super.onCreate(state);

        if (state != null) {
            savedState = state.getBundle(_FRAGMENT_STATE);
        }
    }

    @Override
    public void onDestroyView() {
        savedState = getStateToSave();
        saved = true;

        super.onDestroyView();
    }

    protected Bundle getSavedState() {
        return savedState;
    }

    protected abstract boolean hasSavedState();

    protected abstract Bundle getStateToSave();

}

2) Dans votre Fragment, vous devez avoir ceci :

@Override
protected boolean hasSavedState() {
    Bundle state = getSavedState();

    if (state == null) {
        return false;
    }

    //restaurez vos données ici

    return true;
}

3) Par exemple, vous pouvez appeler hasSavedState dans onActivityCreated :

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

    if (hasSavedState()) {
        return;
    }

    //votre code ici
}

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