Wie speichere ich den Instanzstatus von Fragmenten korrekt im Backstack?

489

Ich habe viele Beispiele für eine ähnliche Frage zu SO gefunden, aber leider entspricht keine Antwort meinen Anforderungen.

Ich habe unterschiedliche Layouts für Hoch- und Querformat und verwende einen Backstack, der mich daran hindert setRetainState(), Konfigurationsänderungsroutinen zu verwenden, und Tricks verwendet.

Ich zeige dem Benutzer bestimmte Informationen in TextViews an, die nicht im Standardhandler gespeichert werden. Beim Schreiben meiner Bewerbung ausschließlich mit Aktivitäten hat Folgendes gut funktioniert:

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

Mit Fragments funktioniert dies nur in sehr spezifischen Situationen. Was schrecklich bricht, ist insbesondere, ein Fragment zu ersetzen, es in den hinteren Stapel zu legen und dann den Bildschirm zu drehen, während das neue Fragment angezeigt wird. Soweit ich verstanden habe, erhält das alte Fragment beim Ersetzen keinen Aufruf, onSaveInstanceState()bleibt aber irgendwie mit dem verbunden, Activityund diese Methode wird später aufgerufen, wenn Viewsie nicht mehr existiert. Suchen Sie also nach einem meiner TextViewErgebnisse in a NullPointerException.

Außerdem fand ich, dass es TextViewsbei Fragments keine gute Idee ist , den Verweis auf my zu behalten , auch wenn es bei s in Ordnung war Activity. In diesem Fall wird onSaveInstanceState()der Status tatsächlich gespeichert, aber das Problem tritt erneut auf, wenn ich den Bildschirm zweimal drehe, wenn das Fragment ausgeblendet ist, da onCreateView()es in der neuen Instanz nicht aufgerufen wird.

Ich dachte an den Staat bei der Rettung onDestroyView()in einem gewissen BundleTyp Klasse Mitglied Element (es ist eigentlich mehr Daten, nicht nur ein TextView) und Speicher dass in , onSaveInstanceState()aber es gibt auch andere Nachteile. In erster Linie, wenn das Fragment wird zur Zeit gezeigt, die um die beiden Funktionen des Aufrufs wird umgekehrt, so dass ich zu Konto für zwei verschiedene Situationen brauchen würde. Es muss eine sauberere und korrekte Lösung geben!

Der Vee
quelle
1
Hier ist auch ein sehr gutes Beispiel mit detaillierten Erklärungen. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam
1
Ich stimme dem Link emunee.com zu. Es hat ein Problem mit der Stick-Benutzeroberfläche für mich gelöst!
Reenactor Rob

Antworten:

541

Um den Instanzstatus von FragmentIhnen korrekt zu speichern, gehen Sie wie folgt vor:

1. Speichern Sie im Fragment den Instanzstatus, indem Sie ihn überschreiben onSaveInstanceState()und wiederherstellen 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. Und wichtiger Punkt in der Aktivität ist, dass Sie die Instanz des Fragments in speichern onSaveInstanceState()und wiederherstellen müssen 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);
    }

}

Hoffe das hilft.

ThanhHH
quelle
7
Das hat bei mir perfekt funktioniert! Keine Problemumgehungen, keine Hacks, macht auf diese Weise nur Sinn. Vielen Dank dafür, stundenlange Suche erfolgreich gemacht. SaveInstanceState () Ihrer Werte in Fragment, dann speichern Sie Fragment in Aktivität mit dem Fragment, dann wiederherstellen :)
MattMatt
77
Was ist mContent?
Wizurd
14
@wizurd mContent ist ein Fragment und verweist auf die Instanz des aktuellen Fragments in der Aktivität.
ThanhHH
13
Können Sie erklären, wie dadurch der Instanzstatus eines Fragments im Backstack gespeichert wird? Das hat das OP gefragt.
Hitmaneidos
51
Dies hängt nicht mit der Frage zusammen, onSaveInstance wird nicht aufgerufen, wenn das Fragment in den Backstack geputtet wird
Tadas Valaitis
87

Dies ist die Art und Weise, wie ich sie gerade benutze ... es ist sehr kompliziert, aber es behandelt zumindest alle möglichen Situationen. Falls jemand interessiert ist.

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

    /* (...) */

}

Alternativ ist es immer möglich, die Daten in passiven Views in Variablen Viewanzuzeigen und die s nur zur Anzeige zu verwenden, um die beiden Dinge synchron zu halten. Ich halte den letzten Teil allerdings nicht für sehr sauber.

Der Vee
quelle
68
Dies ist die beste Lösung, die ich bisher gefunden habe, aber es gibt noch ein (etwas exotisches) Problem: Wenn Sie zwei Fragmente haben Aund Bwo Asich derzeit auf dem Backstack befindet und Bsichtbar ist, verlieren Sie den Zustand A(des Unsichtbaren) eins) wenn Sie die Anzeige zweimal drehen . Das Problem ist, dass onCreateView()nur in diesem Szenario nicht aufgerufen wird onCreate(). Daher onSaveInstanceState()gibt es später keine Ansichten, vor denen der Status gespeichert werden kann. Man müsste den übergebenen Zustand speichern und dann speichern onCreate().
Devconsole
7
@devconsole Ich wünschte, ich könnte dir 5 Stimmen für diesen Kommentar geben! Diese zweimalige Rotation hat mich seit Tagen umgebracht.
DroidT
Vielen Dank für die tolle Antwort! Ich habe jedoch eine Frage. Wo kann das Modellobjekt (POJO) in diesem Fragment am besten instanziiert werden?
Renjith
7
So speichern Hilfe Zeit für andere, App.VSTUPund App.STAVsind beide String - Tags , die repräsentieren die Objekte , die sie zu erhalten versuchen. Beispiel: savedState = savedInstanceState.getBundle(savedGamePlayString);odersavedState.getDouble("averageTime")
Tanner Hallman
1
Das ist eine Sache von Schönheit.
Ivan
61

In der neuesten Support-Bibliothek ist keine der hier diskutierten Lösungen mehr erforderlich. Sie können mit Ihren ActivityFragmenten spielen, wie Sie möchten FragmentTransaction. Stellen Sie einfach sicher, dass Ihre Fragmente entweder mit einer ID oder einem Tag identifiziert werden können.

Die Fragmente werden automatisch wiederhergestellt, solange Sie nicht versuchen, sie bei jedem Aufruf von neu zu erstellen onCreate(). Stattdessen sollten Sie überprüfen, ob savedInstanceStatenicht null ist, und in diesem Fall die alten Verweise auf die erstellten Fragmente finden.

Hier ist ein Beispiel:

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

Beachten Sie jedoch, dass derzeit ein Fehler beim Wiederherstellen des verborgenen Status eines Fragments vorliegt. Wenn Sie Fragmente in Ihrer Aktivität verstecken, müssen Sie diesen Status in diesem Fall manuell wiederherstellen.

Ricardo
quelle
2
Ist Ihnen dieses Update bei der Verwendung der Support-Bibliothek aufgefallen oder haben Sie irgendwo darüber gelesen? Gibt es weitere Informationen, die Sie dazu bereitstellen könnten? Vielen Dank!
Piovezan
1
@Piovezan kann implizit aus den Dokumenten abgeleitet werden. Das Dokument beginTransaction () lautet beispielsweise wie folgt: "Dies liegt daran, dass das Framework dafür sorgt, dass Ihre aktuellen Fragmente im Status (...) gespeichert werden ." Ich habe meine Apps auch schon seit einiger Zeit mit diesem erwarteten Verhalten codiert.
Ricardo
1
@ Ricardo trifft dies zu, wenn ein ViewPager verwendet wird?
Derek Beattie
1
Normalerweise ja, es sei denn, Sie haben das Standardverhalten bei der Implementierung von FragmentPagerAdapteroder geändert FragmentStatePagerAdapter. Wenn Sie sich beispielsweise den Code von ansehen FragmentStatePagerAdapter, werden Sie feststellen, dass die restoreState()Methode die Fragmente aus dem FragmentManagerParameter wiederherstellt, den Sie beim Erstellen des Adapters übergeben haben.
Ricardo
4
Ich denke, dieser Beitrag ist die beste Antwort auf die ursprüngliche Frage. Es ist auch dasjenige, das meiner Meinung nach am besten auf die Funktionsweise der Android-Plattform abgestimmt ist. Ich würde empfehlen, diese Antwort als "Akzeptiert" zu markieren , um zukünftigen Lesern besser zu helfen.
DBM
17

Ich möchte nur die Lösung nennen, die ich gefunden habe und die alle in diesem Beitrag vorgestellten Fälle behandelt, die ich von Vasek und devconsole abgeleitet habe. Diese Lösung behandelt auch den Sonderfall, wenn das Telefon mehr als einmal gedreht wird, während Fragmente nicht sichtbar sind.

Hier speichere ich das Bundle für die spätere Verwendung, da onCreate und onSaveInstanceState die einzigen Aufrufe sind, die ausgeführt werden, wenn das Fragment nicht sichtbar ist

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

Da destroyView in der speziellen Rotationssituation nicht aufgerufen wird, können wir sicher sein, dass wir es verwenden sollten, wenn es den Status erstellt.

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

Dieser Teil wäre der gleiche.

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

Jetzt hier ist der schwierige Teil. In meiner onActivityCreated-Methode instanziiere ich die Variable "myObject", aber die Rotation erfolgt onActivity und onCreateView wird nicht aufgerufen. Daher ist myObject in dieser Situation null, wenn sich die Ausrichtung mehr als einmal dreht. Ich kann dies umgehen, indem ich dasselbe Bundle, das in onCreate gespeichert wurde, wiederverwenden kann wie das ausgehende Bundle.

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

Wo immer Sie den Status wiederherstellen möchten, verwenden Sie einfach das Paket "savedState"

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
DroidT
quelle
Kannst du mir sagen ... Was ist "MyObject" hier?
Kavie
2
Alles was du willst. Dies ist nur ein Beispiel für etwas, das im Bundle gespeichert wird.
DroidT
3

Dank DroidT habe ich Folgendes gemacht:

Mir ist klar, dass wenn das Fragment onCreateView () nicht ausführt, seine Ansicht nicht instanziiert wird. Wenn das Fragment auf dem Backstack seine Ansichten nicht erstellt hat, speichere ich den zuletzt gespeicherten Status. Andernfalls erstelle ich mein eigenes Bundle mit den Daten, die ich speichern / wiederherstellen möchte.

1) Erweitern Sie diese Klasse:

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) In Ihrem Fragment müssen Sie Folgendes haben:

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

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

    //restore your data here

    return true;
}

3) Sie können beispielsweise hasSavedState in onActivityCreated aufrufen:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Noturno
quelle
-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
quelle
1
Nicht im Zusammenhang mit der ursprünglichen Frage.
Jorge E. Hernández