544 Stimmen

Wie speichert man den Instanzzustand von Fragments korrekt im Back-Stack?

Ich habe viele Instanzen einer ähnlichen Frage auf SO gefunden, aber leider entspricht keine Antwort meinen Anforderungen.

Ich habe verschiedene Layouts für Hochformat und Querformat und benutze den Back-Stack, was sowohl die Verwendung von setRetainState() als auch Tricks mit Konfigurationsänderungsroutinen verhindert.

Ich zeige bestimmte Informationen dem Benutzer in TextViews an, die nicht im Standard-Handler gespeichert werden. Als ich meine Anwendung ausschließlich unter Verwendung von Activities geschrieben habe, 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. Insbesondere wird es furchtbar durcheinander, wenn ein Fragment ersetzt, in den Back-Stack gelegt und dann der Bildschirm gedreht wird, während das neue Fragment angezeigt wird. Soweit ich verstanden habe, erhält das alte Fragment keinen Aufruf von onSaveInstanceState() bei einem Ersatz, bleibt aber irgendwie mit der Activity verbunden und diese Methode wird später aufgerufen, wenn seine View nicht mehr existiert, sodass das Suchen nach einem meiner TextViews zu einer NullPointerException führt.

Außerdem habe ich festgestellt, dass es keine gute Idee ist, Referenzen auf meine TextViews mit Fragments zu behalten, auch wenn es mit Activitys in Ordnung war. In diesem Fall speichert onSaveInstanceState() tatsächlich den Zustand, aber das Problem tritt wieder auf, wenn ich den Bildschirm zweimal drehe, wenn das Fragment verborgen ist, da sein onCreateView() im neuen Fall nicht aufgerufen wird.

Ich dachte daran, den Zustand in onDestroyView() in ein Bundle-Artiges Klassenmember-Element zu speichern (es handelt sich tatsächlich um mehr Daten, nicht nur eine TextView) und dieses in onSaveInstanceState() zu speichern, aber es gibt andere Nachteile. Vor allem, wenn das Fragment tatsächlich angezeigt wird, wird die Reihenfolge der Aufrufe der beiden Funktionen umgekehrt, sodass ich zwei verschiedene Situationen berücksichtigen müsste. Es muss eine sauberere und korrekte Lösung geben!

586voto

ThanhHH Punkte 6442

Um den Zustand der Instanz des Fragment korrekt zu speichern, sollten Sie folgendes tun:

1. Im Fragment den Instanzzustand speichern, indem Sie onSaveInstanceState() überschreiben und in onActivityCreated() wiederherstellen:

class MeinFragment erweitert Fragment {

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        ...
        if (savedInstanceState != null) {
            // Den Zustand des Fragments hier wiederherstellen
        }
    }
    ...
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);

        // Den Zustand des Fragments hier speichern
    }

}

2. Und wichtiger Punkt, in der Aktivität müssen Sie die Instanz des Fragments in onSaveInstanceState() speichern und in onCreate() wiederherstellen.

class MeineAktivität erweitert Activity {

    private MeinFragment mMyFragment;

    public void onCreate(Bundle savedInstanceState) {
        ...
        if (savedInstanceState != null) {
            // Die Instanz des Fragments wiederherstellen
            mMyFragment = getSupportFragmentManager().getFragment(savedInstanceState, "meinFragmentName");
            ...
        }
        ...
    }

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

        // Die Instanz des Fragments speichern
        getSupportFragmentManager().putFragment(outState, "meinFragmentName", mMyFragment);
    }

}

93voto

The Vee Punkte 10965

Dies ist eine sehr alte Antwort.

Ich schreibe nicht mehr für Android, daher ist die Funktion in aktuellen Versionen nicht garantiert und es wird keine Updates dazu geben.

Dies ist die Methode, die ich im Moment verwende... sie ist sehr kompliziert, aber zumindest behandelt sie alle möglichen Situationen. Falls es jemanden interessiert.

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

        /* (...) */

        /* Wenn das Fragment zwischendurch (Bildschirmrotation) zerstört wurde, müssen wir zuerst den savedState wiederherstellen */
        /* Wenn es jedoch nicht zerstört wurde, bleibt es in der Instanz des letzten onDestroyView() und wir möchten es nicht überschreiben */
        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 hier sicher definiert*/
        vstup = null;
    }

    private Bundle saveState() { /* aufgerufen entweder von onDestroyView() oder onSaveInstanceState() */
        Bundle state = new Bundle();
        state.putCharSequence(App.VSTUP, vstup.getText());
        return state;
    }

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        /* Wenn onDestroyView() zuerst aufgerufen wird, können wir den zuvor gespeicherten savedState verwenden, aber wir können saveState() nicht mehr aufrufen */
        /* Wenn onSaveInstanceState() zuerst aufgerufen wird, haben wir keinen savedState, daher müssen wir saveState() aufrufen */
        /* => (?:) Operator unvermeidlich! */
        outState.putBundle(App.STAV, (savedState != null) ? savedState : saveState());
    }

    /* (...) */

}

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

68voto

Ricardo Punkte 7425

Bei der neuesten Support-Bibliothek sind keine der hier diskutierten Lösungen mehr notwendig. Sie können mit den Fragmenten Ihrer Activity nach Belieben spielen, indem Sie FragmentTransaction verwenden. 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 versuchen, sie nicht bei jedem Aufruf von onCreate() neu zu erstellen. Stattdessen sollten Sie überprüfen, ob savedInstanceState nicht null ist und in diesem Fall die alten Referenzen zu den erstellten Fragmenten 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 Bug beim Wiederherstellen des ausgeblendeten Zustands eines Fragments besteht. Wenn Sie Fragmente in Ihrer Activity ausblenden, müssen Sie diesen Zustand in diesem Fall manuell wiederherstellen.

17voto

DroidT Punkte 3038

Ich möchte einfach die Lösung präsentieren, die ich entwickelt habe, um mit allen Fällen umzugehen, die in diesem Beitrag erwähnt wurden, und die ich von Vasek und devconsole abgeleitet habe. Diese Lösung behandelt auch den speziellen Fall, wenn das Telefon mehrmals gedreht wird, während die Fragmente nicht sichtbar sind.

Hier speichere ich das Bundle für späteren Gebrauch, da onCreate und onSaveInstanceState die einzigen Aufrufe sind, die gemacht 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 Zustand erstellt.

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

Dieser Teil bleibt gleich.

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

Jetzt kommt der knifflige Teil. In meiner onActivityCreated-Methode instanziiere ich die Variable "myObject", aber die Rotation erfolgt in onActivity und onCreateView wird nicht aufgerufen. Daher wird myObject in dieser Situation null sein, wenn die Ausrichtung mehrmals rotiert. Ich umgehe dies, indem ich dasselbe Bundle wiederverwende, das in onCreate gespeichert wurde, als 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);
}

Jetzt kannst du immer das savedState-Bundle verwenden, um den Zustand wiederherzustellen.

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

5voto

Noturno Punkte 121

Dank DroidT habe ich das gemacht:

Ich habe festgestellt, dass, wenn das Fragment onCreateView() nicht ausführt, seine Ansicht nicht instanziiert ist. Daher, wenn das Fragment im Back-Stack seine Ansichten nicht erstellt hat, speichere ich den zuletzt gespeicherten Zustand, sonst 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 muss folgendes stehen:

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

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

    // Ihre Daten hier wiederherstellen

    return true;
}

3) Zum Beispiel können Sie hasSavedState in onActivityCreated aufrufen:

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

    if (hasSavedState()) {
        return;
    }

    // Ihr Code hier
}

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X