Come salvare correttamente lo stato di istanza dei frammenti nello stack posteriore?


489

Ho trovato molti casi di una domanda simile su SO ma purtroppo nessuna risposta soddisfa i miei requisiti.

Ho diversi layout per verticale e orizzontale e sto usando back stack, che mi impedisce sia di usare setRetainState()che trucchi usando le routine di modifica della configurazione.

Mostro determinate informazioni all'utente in TextViews, che non vengono salvate nel gestore predefinito. Quando ho scritto la mia applicazione esclusivamente utilizzando Attività, ha funzionato bene:

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

Con Fragments, questo funziona solo in situazioni molto specifiche. In particolare, ciò che si rompe in modo orribile è la sostituzione di un frammento, lo mette nella pila posteriore e quindi ruota lo schermo mentre viene mostrato il nuovo frammento. Da quello che ho capito, il vecchio frammento non riceve una chiamata a onSaveInstanceState()quando viene sostituito, ma rimane in qualche modo collegato al Activitye questo metodo viene chiamato più tardi quando Viewnon esiste più, quindi cerca uno qualsiasi dei miei TextViewrisultati in a NullPointerException.

Inoltre, ho scoperto che mantenere il riferimento al mio TextViewsnon è una buona idea con Fragments, anche se è stato OK con Activity. In tal caso, in onSaveInstanceState()realtà salva lo stato ma il problema riappare se ruoto lo schermo due volte quando il frammento è nascosto, poiché onCreateView()non viene chiamato nella nuova istanza.

Ho pensato di salvare lo stato in onDestroyView()in qualche Bundleelemento membro della classe di tipo (in realtà più dati, non solo uno TextView) e risparmio che in onSaveInstanceState()ma ci sono altri inconvenienti. In primo luogo, se il frammento è attualmente mostrato, l'ordine di chiamare le due funzioni è invertito, quindi dovrei tenere conto di due diverse situazioni. Ci deve essere una soluzione più pulita e corretta!


1
Ecco un ottimo esempio con una spiegazione dettagliata. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
Secondo il link emunee.com. Mi ha risolto un problema con l'interfaccia utente del bastone!
Reenactor Rob

Risposte:


541

Per salvare correttamente lo stato dell'istanza, Fragmentè necessario:

1. Nel frammento, salva lo stato dell'istanza eseguendo l'override onSaveInstanceState()e il ripristino in 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. E il punto importante , nell'attività, è necessario salvare l'istanza del frammento onSaveInstanceState()e ripristinarla 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);
    }

}

Spero che sia di aiuto.


7
Ha funzionato perfettamente per me! Nessuna soluzione alternativa, nessun trucco, ha senso in questo modo. Grazie per questo, ore di ricerca di successo. SaveInstanceState () dei tuoi valori in frammento, quindi salva il frammento in Activity tenendo il frammento, quindi ripristina :)
MattMatt

77
Che cos'è mContent?
wizurd,

14
@wizurd mContent è un frammento, è un riferimento all'istanza del frammento corrente nell'attività.
ThanhHH,

13
Puoi spiegare come questo salverà lo stato di istanza di un frammento nello stack posteriore? Questo è ciò che l'OP ha chiesto.
hitmaneidos,

51
Questo non è correlato alla domanda, onSaveInstance non viene chiamato quando il frammento viene messo nello zaino
Tadas Valaitis

87

Questo è il modo in cui sto usando in questo momento ... è molto complicato ma almeno gestisce tutte le possibili situazioni. Nel caso qualcuno fosse interessato.

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

    /* (...) */

}

In alternativa , è sempre possibile mantenere i dati visualizzati in Views passivi in variabili e usare Views solo per visualizzarli, mantenendo le due cose in sincronia. Tuttavia, non considero l'ultima parte molto pulita.


68
Questa è la soluzione migliore che ho trovato finora, ma rimane ancora un problema (un po 'esotico): se hai due frammenti Ae B, dove Aè attualmente sullo backstack ed Bè visibile, perdi lo stato di A(l'invisibile uno) se si ruota il display due volte . Il problema è che onCreateView()non viene chiamato solo in questo scenario onCreate(). Quindi, in seguito, onSaveInstanceState()non ci sono viste da cui salvare lo stato. Uno dovrebbe archiviare e quindi salvare lo stato passato onCreate().
Devconsole,

7
@devconsole Vorrei poterti dare 5 voti in più per questo commento! Questa rotazione due volte mi sta uccidendo da giorni.
DroidT,

Grazie per l'ottima risposta! Ho una domanda però. Qual è il posto migliore per creare un'istanza dell'oggetto modello (POJO) in questo frammento?
Renjith,

7
Per aiutare a risparmiare tempo per gli altri, App.VSTUPe App.STAVsono entrambi tag di stringa che rappresentano gli oggetti che stanno cercando di ottenere. Esempio: savedState = savedInstanceState.getBundle(savedGamePlayString);oppuresavedState.getDouble("averageTime")
Tanner Hallman,

1
Questa è una cosa di bellezza.
Ivan

61

Nell'ultima libreria di supporto nessuna delle soluzioni discusse qui è più necessaria. Puoi giocare con i tuoi Activityframmenti come preferisci usando FragmentTransaction. Assicurati solo che i tuoi frammenti possano essere identificati con un ID o un tag.

I frammenti verranno ripristinati automaticamente finché non si tenta di ricrearli ad ogni chiamata onCreate(). Invece, dovresti controllare se savedInstanceStatenon è null e trovare i vecchi riferimenti ai frammenti creati in questo caso.

Ecco un esempio:

@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);
    }
...
}

Si noti tuttavia che esiste attualmente un bug durante il ripristino dello stato nascosto di un frammento. Se nascondi frammenti nella tua attività, in questo caso dovrai ripristinare questo stato manualmente.


2
Questa correzione è qualcosa che hai notato durante l'utilizzo della libreria di supporto o l'hai letto da qualche parte? Ci sono ulteriori informazioni che potresti fornire al riguardo? Grazie!
Piovezan,

1
@Piovezan può essere dedotto implicitamente dai documenti. Ad esempio, il documento beginTransaction () recita come segue: "Questo perché il framework si occupa di salvare i frammenti correnti nello stato (...)". Ho anche codificato le mie app con questo comportamento previsto per un po 'di tempo.
Ricardo,

1
@Ricardo si applica se si utilizza un ViewPager?
Derek Beattie,

1
Normalmente sì, a meno che non sia stato modificato il comportamento predefinito nella propria implementazione di FragmentPagerAdaptero FragmentStatePagerAdapter. Se osservi il codice di FragmentStatePagerAdapter, ad esempio, vedrai che il restoreState()metodo ripristina i frammenti che FragmentManagerhai passato come parametro durante la creazione dell'adattatore.
Ricardo,

4
Penso che questo contributo sia la migliore risposta alla domanda originale. È anche quello che - secondo me - è meglio allineato con il funzionamento della piattaforma Android. Consiglierei di contrassegnare questa risposta come "accettata" per aiutare meglio i futuri lettori.
dbm

17

Voglio solo dare la soluzione che mi è venuta in mente che gestisce tutti i casi presentati in questo post che ho derivato da Vasek e devconsole. Questa soluzione gestisce anche il caso speciale quando il telefono viene ruotato più di una volta mentre i frammenti non sono visibili.

Ecco dove immagazzino il bundle per un uso successivo poiché onCreate e onSaveInstanceState sono le uniche chiamate che vengono fatte quando il frammento non è visibile

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

Dato che destroyView non viene chiamato nella situazione di rotazione speciale, possiamo essere certi che se crea lo stato dovremmo usarlo.

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

Questa parte sarebbe la stessa.

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

Ora ecco la parte difficile. Nel mio metodo onActivityCreated ho un'istanza della variabile "myObject" ma la rotazione avviene suActivity e onCreateView non vengono chiamati. Pertanto, myObject sarà nullo in questa situazione quando l'orientamento ruota più di una volta. Mi aggiro riutilizzando lo stesso bundle che è stato salvato in onCreate come bundle in uscita.

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

Ora, ovunque tu voglia ripristinare lo stato, usa semplicemente il pacchetto saveState

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

Puoi dirmi ... Che cos'è "MyObject" qui?
Kavie,

2
Qualunque cosa tu voglia che sia. È solo un esempio che rappresenta qualcosa che verrebbe salvato nel pacchetto.
DroidT,

3

Grazie a DroidT , ho fatto questo:

Mi rendo conto che se il frammento non viene eseguito suCreateView (), la sua vista non viene istanziata. Quindi, se il frammento sul back stack non ha creato le sue viste, salvo l'ultimo stato memorizzato, altrimenti creo il mio pacchetto con i dati che voglio salvare / ripristinare.

1) Estendi questa 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) Nel tuo frammento, devi avere questo:

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

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

    //restore your data here

    return true;
}

3) Ad esempio, puoi chiamare hasSavedState in onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}

-6
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();

1
Non correlato alla domanda originale.
Jorge E. Hernández,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.