web-dev-qa-db-fra.com

Une fois pour toutes, comment sauvegarder correctement l'état de l'instance des fragments dans la pile arrière?

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

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

Je montre 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 uniquement à l'aide d'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, c’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() lors de son remplacement mais reste en quelque sorte lié au Activity et cette méthode est appelée ultérieurement lorsque sa View n'existe plus. , recherchez donc l'un de mes résultats TextViews dans un NullPointerException.

De plus, j'ai trouvé que garder la référence à mes TextViews n'est pas une bonne idée avec Fragments, même si c'était OK avec Activity. 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 masqué, car son onCreateView() ne sera pas appelé dans le nouveau. exemple.

J'ai pensé sauvegarder l'état dans onDestroyView() dans un élément de membre de classe de type Bundle (c'est en fait plus de données, pas seulement un TextView) et enregistrer que dans onSaveInstanceState() mais il y a d'autres inconvénients. Principalement, si le fragment is actuellement affiché, l'ordre d'appel des deux fonctions est inversé, il me faudrait donc rendre compte de deux situations différentes. Il doit y avoir une solution plus propre et correcte!

448
The Vee

Pour enregistrer correctement l'état d'instance de Fragment, procédez comme suit:

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

@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 la restaurer dans onCreate().

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

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

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

J'espère que cela t'aides.

508
ThanhHH

C’est ainsi que j’utilise actuellement… C’est très compliqué, mais au moins, il 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 garder les données affichées en mode passif Views dans les variables et d'utiliser le Views uniquement pour les afficher, en maintenant les deux éléments synchronisés. Je ne considère cependant pas la dernière partie comme très propre.

79
The Vee

Dans la dernière bibliothèque de support, aucune des solutions présentées ici n’est plus nécessaire. Vous pouvez jouer avec les fragments de votre Activity comme bon vous semble 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 de onCreate(). Au lieu de cela, vous devriez vérifier si savedInstanceState n'est pas null et rechercher 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 bug lors de la restauration de l'état masqué d'un fragment. Si vous masquez des fragments dans votre activité, vous devrez restaurer cet état manuellement dans ce cas.

53
Ricardo

Je veux juste donner la solution que j'ai proposée qui traite 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 particulier où le téléphone est pivoté plusieurs fois sans que les fragments ne soient visibles.

Voici où je stocke le paquet pour une utilisation ultérieure, puisque 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);
    }
}

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 la plus délicate. Dans ma méthode onActivityCreated, j'instancie la variable "myObject" mais la rotation a lieu onActivity et onCreateView ne sont pas appelées. Par conséquent, myObject sera nul dans cette situation lorsque l'orientation pivotera plusieurs fois. Je contourne cela en réutilisant le même paquet qui a été enregistré dans onCreate en tant que paquet 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 paquet savedState

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

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

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, j'enregistre le dernier état stocké, sinon je construis mon propre ensemble avec les données que je veux sauvegarder/restaurer.

1) Étendre 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
}
3
Noturno