Як правильно зберегти стан екземплярів фрагментів у задній стеці?


489

Я знайшов багато випадків подібного питання щодо SO, але жодна відповідь, на жаль, не відповідає моїм вимогам.

У мене різні схеми для портретного та ландшафтного режиму, і я використовую задній стек, який і заважає мені використовувати, setRetainState()і хитрощі, використовуючи процедури зміни конфігурації.

Я показую певну інформацію користувачеві в TextViews, яка не зберігається в обробнику за замовчуванням. Коли я писав мою заявку виключно за допомогою "Діяльності", то добре працювало:

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

З Fragments це працює лише у дуже конкретних ситуаціях. Зокрема, те, що жахливо ламається, - це заміна фрагмента, поміщення його в задній стек, а потім обертання екрана під час показу нового фрагмента. З того, що я зрозумів, старий фрагмент не отримує дзвінка, onSaveInstanceState()коли його замінюють, але залишається якимось чином пов'язаним із, Activityі цей метод викликається пізніше, коли його Viewвже не існує, тому шукаю будь-який з моїх TextViewрезультатів в a NullPointerException.

Крім того, я виявив, що зберігати посилання на моє TextViewsне є хорошою ідеєю для Fragments, навіть якщо це було нормально з Activity's. У цьому випадку onSaveInstanceState()фактично зберігається стан, але проблема з’являється знову, якщо я повернути екран двічі, коли фрагмент буде приховано, оскільки його onCreateView()не буде викликано в новому екземплярі.

Я думав про збереження стану в onDestroyView()в який - то Bundle-типу елемент члена класу (це на самому справі більше даних, а не тільки один TextView) і економії , що в onSaveInstanceState()але є й інші недоліки. В першу чергу, якщо фрагмент в даний час показано, порядок виклику двох функцій відновлюється, так що я повинен був би рахунок для двох різних ситуацій. Має бути більш чисте і правильне рішення!


1
Ось дуже хороший приклад з детальним поясненням. emuneee.com/blog/2013/01/07/saving-fragment-states
Ісам

1
Я другий за посиланням emunee.com. Це вирішило для мене проблему з інтерфейсом палиці!
Реконструктор Роб

Відповіді:


541

Щоб правильно зберегти стан екземпляра, Fragmentслід зробити наступне:

1. У фрагменті збережіть стан екземпляра за допомогою перестановки onSaveInstanceState()та відновлення в 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. І найважливіший момент у цій діяльності, ви повинні зберегти екземпляр фрагмента в onSaveInstanceState()і відновити 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);
    }

}

Сподіваюсь, це допомагає.


7
Це прекрасно працювало для мене! Жодних обхідних шляхів, жодних злому, цей сенс має сенс. Дякую за це, зробили години пошуку успішними. SaveInstanceState () ваших значень у фрагменті, а потім збережіть фрагмент у Activity, утримуючи фрагмент, а потім відновіть :)
MattMatt

77
Що таке mContent?
wizurd

14
@wizurd mContent - це фрагмент, це посилання на екземпляр поточного фрагмента в діяльності.
ThanhHH

13
Чи можете ви пояснити, як це збереже стан екземпляра фрагмента в задній стек? Ось що запитувала ОП.
hitmaneidos

51
Це не пов’язано з питанням, onSaveInstance не називається, коли фрагмент буде поставлений у тушшю
Tadas

87

Цим я зараз користуюся ... Це дуже складно, але принаймні він справляється з усіма можливими ситуаціями. У випадку, якщо хтось зацікавлений.

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

    /* (...) */

}

Крім того , це завжди можливість зберігати дані, що відображаються в пасивних Views у змінних, і використовувати Views лише для їх відображення, зберігаючи дві речі синхронізованими. Я не вважаю останню частину дуже чистою.


68
Це найкраще рішення, яке я знайшов поки що, але все ще залишається одна (дещо екзотична) проблема: якщо у вас є два фрагменти, Aі Bтам, де Aзараз перебуває у рюкзаку і Bвидно, ви втрачаєте стан A(невидимий один) якщо два рази обертати дисплей . Проблема полягає в тому, onCreateView()що не називається тільки в цьому сценарії onCreate(). Тож пізніше в Росії onSaveInstanceState()немає поглядів, щоб врятувати державу. Треба було б зберігати та зберігати переданий стан onCreate().
devconsole

7
@devconsole Я хотів би, щоб я міг дати вам 5 голосів за цей коментар! Ця річ у два рази обертала мене цілими днями.
DroidT

Дякую за чудову відповідь! У мене є одне питання, хоча. Де в цьому фрагменті найкраще розмістити об'єкт моделі (POJO)?
Renjith

7
Щоб заощадити час для інших, App.VSTUPі App.STAVобидва теги рядків представляють об'єкти, які вони намагаються отримати. Приклад: savedState = savedInstanceState.getBundle(savedGamePlayString);абоsavedState.getDouble("averageTime")
Tanner Hallman

1
Це справа краси.
Іван

61

В останній бібліотеці підтримки жодне з розглянутих тут рішень більше не потрібне. Ви можете грати з вашими Activityфрагментами, як вам подобається, використовуючи FragmentTransaction. Просто переконайтеся, що ваші фрагменти можна ідентифікувати або з ідентифікатором, або з тегом.

Фрагменти будуть відновлені автоматично, доки ви не намагатиметесь відтворити їх під час кожного дзвінка onCreate(). Натомість слід перевірити, чи savedInstanceStateне є нульовим, і знайти старі посилання на створені фрагменти в цьому випадку.

Ось приклад:

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

Однак зауважте, що наразі є помилка під час відновлення прихованого стану фрагмента. Якщо ви ховаєте фрагменти у своїй діяльності, у цьому випадку вам потрібно буде відновити цей стан вручну.


2
Це виправлення, що ви помітили, використовуючи бібліотеку підтримки, чи читали про це десь? Чи є ще якась інформація, яку ви могли б надати про це? Дякую!
Півевезан

1
@Piovezan це може бути неявно виведено з документів. Наприклад, doc beginTransaction () звучить так: "Це тому, що фреймворк піклується про збереження ваших поточних фрагментів у стані (...)". Я також кодую свої програми з такою очікуваною поведінкою вже досить давно.
Рікардо

1
@ Ricardo це застосовується, якщо ви використовуєте ViewPager?
Дерек Бітті

1
Як правило, так, якщо ви не змінили поведінку за замовчуванням під час здійснення FragmentPagerAdapterабо FragmentStatePagerAdapter. Наприклад, якщо ви подивитеся на код FragmentStatePagerAdapter, наприклад, ви побачите, що restoreState()метод відновлює фрагменти з FragmentManagerпереданого вами параметра при створенні адаптера.
Рікардо

4
Я думаю, що цей внесок є найкращою відповіддю на початкове запитання. Це також той, який, на мою думку, найкраще узгоджується з тим, як працює платформа Android. Я рекомендую позначити цю відповідь як "Прийняту", щоб краще допомогти майбутнім читачам.
дбм

17

Я просто хочу вирішити, що я придумав, що розглядає всі випадки, представлені в цій публікації, які я отримав з Васека та девконсоли. Це рішення також стосується особливого випадку, коли телефон обертається не один раз, поки фрагменти не видно.

Ось, якщо я зберігаю пакет для подальшого використання, оскільки onCreate і onSaveInstanceState - це єдині дзвінки, які здійснюються, коли фрагмент не видно

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

Оскільки killView не викликається в особливій обертовій ситуації, ми можемо бути впевнені, що якщо він створює стан, ми повинні його використовувати.

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

Ця частина була б однаковою.

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

Тепер ось складна частина. У своєму методі onActivityCreate я інстанціює змінну "myObject", але обертання відбувається onActivity і onCreateView не викликається. Тому мій об'єкт буде недійсним у цій ситуації, коли орієнтація обертається більше одного разу. Я долаю це, використовуючи той самий пакет, який було збережено в onCreate як і вихідний пакет.

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

Тепер, де ви хочете відновити стан, просто використовуйте пакет збережених держав

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

Ви можете мені сказати ... Що тут "MyObject"?
kavie

2
Все, що ви хочете, щоб це було. Це лише приклад, який представляє щось, що було б збережено в пакеті.
DroidT

3

Завдяки DroidT я зробив це:

Я усвідомлюю, що якщо фрагмент не виконує onCreateView (), його погляд не буде миттєвим. Отже, якщо фрагмент із зворотного стеку не створив його подання, я зберігаю останній збережений стан, інакше будую власний пакет із даними, які хочу зберегти / відновити.

1) Розгорніть цей клас:

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) У своєму фрагменті ви повинні мати це:

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

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

    //restore your data here

    return true;
}

3) Наприклад, ви можете викликати hasSavedState у onActivityCreate:

@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
Не пов'язане з оригінальним питанням.
Хорхе Ернандес
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.