Jak poprawnie zapisać stan instancji Fragmentów na tylnym stosie?


489

Znalazłem wiele przykładów podobnych pytań dotyczących SO, ale żadna odpowiedź niestety nie spełnia moich wymagań.

Mam różne układy dla portretu i krajobrazu i używam tylnego stosu, co zarówno uniemożliwia mi używanie, jak setRetainState()i sztuczki przy użyciu procedur zmiany konfiguracji.

Wyświetlam pewne informacje użytkownikowi w TextViews, które nie są zapisywane w domyślnym module obsługi. Podczas pisania mojej aplikacji wyłącznie przy użyciu działań następujące działały dobrze:

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

W przypadku Fragments działa to tylko w bardzo specyficznych sytuacjach. W szczególności, co strasznie się psuje, to zastępowanie fragmentu, umieszczanie go na tylnym stosie, a następnie obracanie ekranu podczas wyświetlania nowego fragmentu. Z tego, co zrozumiałem, stary fragment nie otrzymuje wywołania, onSaveInstanceState()gdy jest zastępowany, ale pozostaje w jakiś sposób powiązany z, Activitya ta metoda jest wywoływana później, gdy Viewjuż nie istnieje, więc szukam dowolnego z moich TextViewwyników w NullPointerException.

Odkryłem również, że zachowanie odniesienia do mojego TextViewsnie jest dobrym pomysłem w przypadku Fragments, nawet jeśli było w porządku z Activitys. W takim przypadku onSaveInstanceState()faktycznie zapisuje stan, ale problem pojawia się ponownie, jeśli obrócę ekran dwukrotnie, gdy fragment jest ukryty, ponieważ onCreateView()nie zostanie wywołany w nowej instancji.

Myślałam o zapisaniu stanu w onDestroyView()do pewnego Bundletypu a elementem członkiem klasy (to faktycznie więcej danych, a nie tylko jeden TextView) i zapisując to w onSaveInstanceState()ale istnieją inne wady. Przede wszystkim, jeśli fragment jest obecnie wyświetlany, kolejność wywoływania dwóch funkcji jest odwrócona, więc musiałbym wziąć pod uwagę dwie różne sytuacje. Musi być czystsze i prawidłowe rozwiązanie!


1
Oto bardzo dobry przykład ze szczegółowym wyjaśnieniem. emuneee.com/blog/2013/01/07/saving-fragment-states
Hesam

1
Popieram link emunee.com. Rozwiązało to dla mnie problem interfejsu użytkownika!
Reenactor Rob

Odpowiedzi:


541

Aby poprawnie zapisać stan instancji Fragment, wykonaj następujące czynności:

1. We fragmencie zapisz stan instancji, zastępując onSaveInstanceState()i przywracając w 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. I ważne jest , aby w ćwiczeniu zapisać instancję fragmentu onSaveInstanceState()i przywrócić 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);
    }

}

Mam nadzieję że to pomoże.


7
To działało idealnie dla mnie! Żadnych obejść, żadnych hacków, po prostu ma sens w ten sposób. Dziękujemy za to, że godziny poszukiwań zakończyły się powodzeniem. SaveInstanceState () twoich wartości w fragmencie, następnie zapisz fragment w Activity, trzymając fragment, a następnie przywróć :)
MattMatt

77
Co to jest mContent?
wizurd

14
@wizurd mContent to Fragment, to odniesienie do wystąpienia bieżącego fragmentu w działaniu.
ThanhHH

13
Czy możesz wyjaśnić, jak to zapisze stan wystąpienia fragmentu na tylnym stosie? Właśnie o to poprosił OP.
hitmaneidos

51
Nie ma to związku z pytaniem, onSaveInstance nie jest wywoływany, gdy fragment jest odkładany do tyłu
Tadas Valaitis

87

Tego właśnie używam w tej chwili ... jest to bardzo skomplikowane, ale przynajmniej obsługuje wszystkie możliwe sytuacje. W razie zainteresowania.

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

    /* (...) */

}

Alternatywnie , zawsze jest możliwość utrzymania danych wyświetlanych w pasywnych Viewsw zmiennych i używania Views tylko do ich wyświetlania, utrzymując synchronizację dwóch elementów. Jednak nie uważam ostatniej części za bardzo czystą.


68
Jest to najlepsze rozwiązanie, jakie do tej pory znalazłem, ale wciąż pozostaje jeden (nieco egzotyczny) problem: jeśli masz dwa fragmenty, Aa Bgdzie Aznajduje się on obecnie w plecaku i Bjest widoczny, tracisz stan A(niewidzialnego) jeden), jeśli obrócisz wyświetlacz dwukrotnie . Problem polega na tym, że onCreateView()nie jest wywoływany tylko w tym scenariuszu onCreate(). Więc później onSaveInstanceState()nie ma żadnych widoków, z których można by zapisać stan. Trzeba będzie zapisać, a następnie zapisać przekazany stan onCreate().
devconsole

7
@devconsole Chciałbym móc dać ci 5 głosów za ten komentarz! Ta rotacja dwa razy zabija mnie od wielu dni.
DroidT

Dziękuję za świetną odpowiedź! Mam jednak jedno pytanie. Gdzie jest najlepsze miejsce do utworzenia obiektu modelu (POJO) w tym fragmencie?
Renjith

7
Aby pomóc zaoszczędzić czas dla innych, App.VSTUPi App.STAVto zarówno tagi ciąg, który reprezentuje obiekty starają się uzyskać. Przykład: savedState = savedInstanceState.getBundle(savedGamePlayString);lubsavedState.getDouble("averageTime")
Tanner Hallman

1
To jest piękno.
Ivan

61

W najnowszej bibliotece wsparcia żadne z omawianych tutaj rozwiązań nie jest już konieczne. Możesz bawić się swoimi Activityfragmentami tak, jak lubisz, używając FragmentTransaction. Upewnij się tylko, że twoje fragmenty można zidentyfikować za pomocą identyfikatora lub tagu.

Fragmenty zostaną przywrócone automatycznie, o ile nie spróbujesz ich odtworzyć przy każdym wywołaniu onCreate(). Zamiast tego powinieneś sprawdzić, czy savedInstanceStatenie ma wartości null, i w tym przypadku znaleźć stare odniesienia do utworzonych fragmentów.

Oto przykład:

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

Zauważ jednak, że obecnie występuje błąd podczas przywracania ukrytego stanu fragmentu. Jeśli ukrywasz fragmenty w swojej działalności, w takim przypadku musisz ręcznie przywrócić ten stan.


2
Czy to poprawka zauważyłaś podczas korzystania z biblioteki wsparcia, czy gdzieś o tym czytałeś? Czy możesz podać więcej informacji na ten temat? Dzięki!
Piovezan

1
@Piovezan może to być pośrednio wywnioskowane z dokumentów. Na przykład dokument beginTransaction () brzmi następująco: „Dzieje się tak, ponieważ środowisko zajmuje się zapisywaniem bieżących fragmentów w stanie (...)”. Od dłuższego czasu koduję również moje aplikacje za pomocą tego oczekiwanego zachowania.
Ricardo

1
@ Ricardo ma to zastosowanie, jeśli używasz ViewPager?
Derek Beattie

1
Zwykle tak, chyba że zmieniłeś domyślne zachowanie przy implementacji FragmentPagerAdapterlub FragmentStatePagerAdapter. Jeśli spojrzysz na przykład na kod FragmentStatePagerAdapter, zobaczysz, że restoreState()metoda przywraca fragmenty z FragmentManagerprzekazanego jako parametr podczas tworzenia adaptera.
Ricardo

4
Myślę, że ten wkład jest najlepszą odpowiedzią na pierwotne pytanie. Jest to również ten, który - moim zdaniem - najlepiej pasuje do sposobu działania platformy Android. Poleciłbym zaznaczyć odpowiedź jako „Zaakceptowaną”, aby lepiej pomóc przyszłym czytelnikom.
dbm

17

Chcę tylko podać rozwiązanie, które wymyśliłem, które obsługuje wszystkie przypadki przedstawione w tym poście, które wyprowadziłem z Vasek i devconsole. To rozwiązanie obsługuje również specjalny przypadek, gdy telefon jest obracany więcej niż raz, a fragmenty nie są widoczne.

Oto, gdzie przechowuję pakiet do późniejszego wykorzystania, ponieważ onCreate i onSaveInstanceState to jedyne wywołania, które są wykonywane, gdy fragment nie jest widoczny

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

Ponieważ destroyView nie jest wywoływany w sytuacji specjalnej rotacji, możemy być pewni, że jeśli stworzy on stan, powinniśmy go użyć.

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

Ta część byłaby taka sama.

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

Teraz tutaj jest skomplikowana część. W mojej metodzie onActivityCreated tworzę instancję zmiennej „myObject”, ale obrót odbywa się na onActivity, a onCreateView nie zostaje wywołany. Dlatego myObject będzie zerowy w tej sytuacji, gdy orientacja zostanie obrócona więcej niż jeden raz. Obejście tego problemu polega na ponownym użyciu tego samego pakietu, który został zapisany w programie onCreate jako pakietu wychodzącego.

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

Teraz gdziekolwiek chcesz przywrócić stan, po prostu użyj pakietu saveState

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

Czy możesz mi powiedzieć ... Co to jest „MyObject”?
kavie

2
Wszystko, co chcesz. To tylko przykład reprezentujący coś, co można zapisać w pakiecie.
DroidT

3

Dzięki DroidT zrobiłem to:

Zdaję sobie sprawę, że jeśli Fragment nie wykona funkcji onCreateView (), jego widok nie zostanie utworzony. Jeśli więc fragment na tylnym stosie nie utworzył swoich widoków, zapisuję ostatni zapisany stan, w przeciwnym razie buduję własny pakiet z danymi, które chcę zapisać / przywrócić.

1) Rozszerz tę klasę:

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) W swoim fragmencie musisz mieć to:

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

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

    //restore your data here

    return true;
}

3) Na przykład możesz wywołać hasSavedState w 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
Nie dotyczy pierwotnego pytania.
Jorge E. Hernández
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.