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

489

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

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

J'affiche certaines informations à l'utilisateur dans TextViews, qui ne sont pas enregistrées dans le gestionnaire par défaut. Lors de la rédaction de ma candidature en utilisant uniquement des activités, les éléments suivants ont 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 Fragments, cela ne fonctionne que dans des situations très spécifiques. Plus précisément, ce qui casse horriblement est de remplacer un fragment, de le placer dans la pile arrière, puis de faire pivoter l'écran pendant 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 en quelque sorte lié à la Activityet cette méthode est appelée plus tard lorsqu'elle Viewn'existe plus, donc je cherche l'un de mes TextViewrésultats dans a NullPointerException.

De plus, j'ai trouvé que garder la référence à mon TextViewsn'est pas une bonne idée avec Fragments, même si ça allait avec Activity. Dans ce cas, onSaveInstanceState()enregistre réellement l'état mais le problème réapparaît si je fais pivoter l'écran deux fois lorsque le fragment est masqué, car il onCreateView()n'est pas appelé dans la nouvelle instance.

Je pensais que de sauver l'état onDestroyView()dans un certain Bundleélément de membre de classe de type (il est en fait plus de données, pas seulement un TextView) et les économies que dans , onSaveInstanceState()mais il y a d' autres inconvénients. Principalement, si le fragment est actuellement affiché, l'ordre d'appeler les deux fonctions est inversé, donc je devrais tenir compte de deux situations différentes. Il doit y avoir une solution plus propre et correcte!

The Vee
la source
1
Voici également un très bon exemple avec une explication détaillée. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam
1
J'appuie le lien emunee.com. Cela a résolu un problème d'interface utilisateur pour moi!
Reenactor Rob

Réponses:

541

Pour enregistrer correctement l'état de l'instance, Fragmentprocédez comme suit:

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

class MyFragment extends Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            //Restore the fragment's state here
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        //Save the fragment's state here
    }

}

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 

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

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

        //Save the fragment's instance
        getSupportFragmentManager().putFragment(outState, "myFragmentName", mMyFragment);
    }

}

J'espère que cela t'aides.

ThanhHH
la source
7
Cela a parfaitement fonctionné pour moi! Aucune solution de contournement, aucun piratage, est tout simplement logique de cette façon. Merci pour cela, fait des heures de recherche réussies. SaveInstanceState () de vos valeurs dans fragment, puis enregistrez le fragment dans Activity contenant le fragment, puis restaurez :)
MattMatt
77
Qu'est-ce que mContent?
wizurd
14
@wizurd mContent est un fragment, c'est une référence à l'instance du fragment actuel dans l'activité.
ThanhHH
13
Pouvez-vous expliquer comment cela sauvera l'état de l'instance d'un fragment dans la pile arrière? C'est ce que le PO a demandé.
hitmaneidos
51
Ceci n'est pas lié à la question, onSaveInstance n'est pas appelé lorsque le fragment est mis en backstack
Tadas Valaitis
87

C'est la façon dont j'utilise en ce moment ... c'est très compliqué mais au moins ça 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);

        /* (...) */

        /* If the Fragment was destroyed inbetween (screen rotation), we need to recover the savedState first */
        /* However, if it was not, it stays in the instance from the last onDestroyView() and we don't want to overwrite it */
        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 defined here for sure */
        vstup = null;
    }

    private Bundle saveState() { /* called either from onDestroyView() or onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* If onDestroyView() is called first, we can use the previously savedState but we can't call saveState() anymore */
        /* If onSaveInstanceState() is called first, we don't have savedState, so we need to call saveState() */
        /* => (?:) operator inevitable! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

Alternativement , il est toujours possible de conserver les données affichées dans des Views passifs dans des variables et d'utiliser les Views uniquement pour les afficher, en gardant les deux choses synchronisées. Cependant, je ne considère pas la dernière partie très propre.

The Vee
la source
68
C'est la meilleure solution que j'ai trouvée jusqu'à présent, mais il reste un problème (quelque peu exotique): si vous avez deux fragments, Aet B, où Aest actuellement sur le backstack et Best visible, alors vous perdez l'état de A(l'invisible un) si vous tournez l'affichage deux fois . Le problème est que ce onCreateView()n'est pas appelé uniquement dans ce scénario onCreate(). Donc, plus tard, onSaveInstanceState()il n'y a aucune vue pour enregistrer l'état. Il faudrait stocker puis sauvegarder l'état passé onCreate().
devconsole
7
@devconsole J'aimerais pouvoir vous donner 5 votes pour ce commentaire! Cette rotation deux fois me tue depuis des jours.
DroidT
Merci pour la bonne réponse! J'ai quand même une question. Quel est le meilleur endroit pour instancier un objet modèle (POJO) dans ce fragment?
Renjith
7
Pour gagner du temps pour les autres, App.VSTUPet App.STAVsont les deux balises de chaîne qui représentent les objets qu'ils tentent d'obtenir. Exemple: savedState = savedInstanceState.getBundle(savedGamePlayString);ousavedState.getDouble("averageTime")
Tanner Hallman
1
C'est une chose de beauté.
Ivan
61

Sur la dernière bibliothèque de support, aucune des solutions discutées ici n'est plus nécessaire. Vous pouvez jouer avec vos Activityfragments comme vous le souhaitez en utilisant le FragmentTransaction. Assurez-vous simplement que vos fragments peuvent être identifiés avec un identifiant ou une balise.

Les fragments seront restaurés automatiquement tant que vous n'essayez pas de les recréer à chaque appel à onCreate(). Au lieu de cela, vous devez vérifier si savedInstanceStaten'est pas nul et trouver 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);
    }
...
}

Notez cependant qu'il existe actuellement un bogue lors de la restauration de l'état caché d'un fragment. Si vous cachez des fragments dans votre activité, vous devrez restaurer cet état manuellement dans ce cas.

Ricardo
la source
2
Ce problème est-il quelque chose que vous avez remarqué en utilisant la bibliothèque de support ou avez-vous lu quelque chose à ce sujet quelque part? Y a-t-il d'autres informations que vous pourriez fournir à ce sujet? Merci!
Piovezan
1
@Piovezan, cela peut être implicitement déduit des documents. Par exemple, le document beginTransaction () se lit comme suit: "C'est parce que le framework s'occupe de sauvegarder vos fragments actuels dans l'état (...)". J'ai également codé mes applications avec ce comportement attendu depuis un certain temps maintenant.
Ricardo
1
@Ricardo, cela s'applique-t-il si vous utilisez un ViewPager?
Derek Beattie
1
Normalement oui, sauf si vous avez modifié le comportement par défaut sur votre implémentation de FragmentPagerAdapterou FragmentStatePagerAdapter. Si vous regardez le code de FragmentStatePagerAdapter, par exemple, vous verrez que la restoreState()méthode restaure les fragments du FragmentManagerparamètre que vous avez transmis lors de la création de l'adaptateur.
Ricardo
4
Je pense que cette contribution est la meilleure réponse à la question initiale. C'est aussi celui qui, à mon avis, est le mieux aligné sur le fonctionnement de la plate-forme Android. Je recommanderais de marquer cette réponse comme étant «acceptée» pour mieux aider les futurs lecteurs.
dbm
17

Je veux juste donner la solution que j'ai trouvée qui gère tous les cas présentés dans ce post que j'ai dérivés de Vasek et de 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 seuls appels effectués lorsque le fragment n'est pas visible

MyObject myObject;
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);
    }
}

Étant donné que destroyView n'est pas appelé dans la situation de rotation spéciale, nous pouvons être certains que s'il crée l'état, nous devons 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 la plus 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, myObject sera nul dans cette situation lorsque l'orientation pivote plusieurs fois. Je contourne cela en réutilisant le même bundle qui a été enregistré dans onCreate que le 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, où que vous vouliez restaurer l'état, utilisez simplement le bundle saveState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
DroidT
la source
Pouvez-vous me dire ... Qu'est-ce que "MyObject" ici?
kavie
2
Tout ce que vous voulez que ce soit. C'est juste un exemple représentant quelque chose qui serait enregistré dans le bundle.
DroidT
3

Grâce à DroidT , j'ai réalisé ceci:

Je me rends compte 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, j'enregistre le dernier état stocké, sinon je crée mon propre bundle avec les données que je veux enregistrer / 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;
    }

    //restore your data here

    return true;
}

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

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Noturno
la source
-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();
Nilesh Savaliya
la source
1
Pas lié à la question d'origine.
Jorge E. Hernández