Raz na zawsze, jak poprawnie zapisać stan instancji fragmentów w tylnym stosie?

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

Mam różne układy dla pionowego i poziomego i używam tylnego stosu, co uniemożliwia mi używanie setRetainState() i sztuczek przy użyciu procedur zmiany konfiguracji.

Pokazuję użytkownikowi pewne informacje w TextViews, które nie są zapisywane w domyślnym programie obsługi. Podczas pisania mojej aplikacji wyłącznie za pomocą czynności, działały następujące cóż:

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

Z Fragment s, działa to tylko w bardzo specyficznych sytuacjach. W szczególności, to, co straszliwie się psuje, to wymiana fragmentu, umieszczenie go w tylnym stosie, a następnie obrócenie ekranu podczas pokazywania nowego fragmentu. Z tego, co zrozumiałem, Stary fragment nie odbiera połączenia do onSaveInstanceState() podczas zastępowania, ale pozostaje w jakiś sposób połączony z Activity i ta metoda jest wywoływana później, gdy jej View już nie istnieje, więc szukanie któregokolwiek z moich TextViewskutkuje NullPointerException.

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

Myślałem o zapisaniu stanu w onDestroyView() do jakiegoś elementu klasy Bundle-type (w rzeczywistości jest to więcej danych, a nie tylko jeden TextView) i zapisaniu to W onSaveInstanceState() ale są inne wady. Przede wszystkim, jeśli fragment jest obecnie pokazany, kolejność wywołania obu funkcji jest odwrócona, więc musiałbym uwzględnić dwie różne sytuacje. Musi być czystsze i prawidłowe rozwiązanie!

Author: The Vee, 2013-03-09

6 answers

Aby poprawnie zapisać stan instancji Fragment należy wykonać następujące czynności:

1. w fragmencie Zapisz stan instancji przez nadpisanie onSaveInstanceState() i przywróć w 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. i ważny punkt , w aktywności należy zapisać instancję fragmentu w onSaveInstanceState() i przywrócić w 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);
}
Mam nadzieję, że to pomoże.
 465
Author: ThanhHH,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-02-16 12:43:38

To jest sposób, w jaki używam w tej chwili... to bardzo skomplikowane, ale przynajmniej radzi sobie ze wszystkimi możliwymi sytuacjami. Gdyby ktoś był zainteresowany.

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 istnieje możliwość przechowywania danych wyświetlanych w pasywnych View s W zmiennych i używania View s Tylko do ich wyświetlania, zachowując synchronizację obu rzeczy. Nie uważam jednak ostatniej części za czystą.

 75
Author: The Vee,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-06-25 15:04:22

W najnowszej bibliotece wsparcia żadne z omawianych rozwiązań nie jest już konieczne. Możesz grać swoimi fragmentami Activity, Jak chcesz, używając FragmentTransaction. Po prostu upewnij się, że 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 savedInstanceState nie jest null i znaleźć stare odniesienia do utworzonych fragmentów w tym przypadku.

Tutaj jest przykładem:

@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 aktywności, w tym przypadku musisz ręcznie przywrócić ten stan.

 48
Author: Ricardo Lage,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-06 21:32:15

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

Oto, gdzie przechowuję pakiet do późniejszego wykorzystania, ponieważ onCreate i onSaveInstanceState są jedynymi wywołaniami, 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 specjalnym sytuacja rotacyjna możemy być pewni, że jeśli tworzy 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;
}

Teraztutaj jest trudna część. W mojej metodzie onActivityCreated inicjuję zmienną "myObject", ale rotacja dzieje się onActivity i onCreateView nie jest wywoływana. Dlatego myObject będzie null w tej sytuacji, gdy orientacja obraca się więcej niż jeden raz. Obejść to poprzez ponowne użycie tego samego pakietu, który został zapisany w onCreate jako out going 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);
}

Teraz, gdziekolwiek chcesz przywrócić stan, użyj pakietu savedState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
 15
Author: DroidT,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-11-02 17:04:07

Dzięki DroidT , zrobiłem to:

Zdaję sobie sprawę, że jeśli Fragment nie wykona oncreateview (), jego widok nie zostanie utworzony. Tak więc, jeśli fragment na tylnym stosie nie stworzył 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 Twoim 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żna wywołać hasSavedState w onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
 3
Author: Noturno,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-08-24 18:58:59
final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();
 0
Author: Nilesh Savaliya,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-21 06:45:31