Как правильно сохранить состояние экземпляра фрагментов в заднем стеке?

489

Я нашел много случаев аналогичного вопроса о SO, но, к сожалению, ни один ответ не отвечает моим требованиям.

У меня есть разные макеты для портретной и альбомной ориентации, и я использую задний стек, который и мешает мне использовать setRetainState()трюки, используя процедуры изменения конфигурации.

Я показываю определенную информацию пользователю в TextViews, которая не сохраняется в обработчике по умолчанию. При написании моего приложения исключительно с использованием Activity, хорошо работало следующее:

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результатов в NullPointerException.

Кроме того, я обнаружил, что сохранение ссылки на my TextViewsне является хорошей идеей с Fragments, даже если это было нормально с Activitys. В этом случае 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);
    }

}

Надеюсь это поможет.

ThanhHH
источник
7
Это отлично сработало для меня! Никаких обходных путей, никаких взломов, просто так имеет смысл. Спасибо за это, сделали часы поиска успешными. SaveInstanceState () ваших значений во фрагменте, затем сохраните фрагмент в Activity, удерживая фрагмент, затем восстановите :)
MattMatt
77
Что такое mContent?
Визурд
14
@wizurd mContent - это фрагмент, это ссылка на экземпляр текущего фрагмента в действии.
ThhhhH
13
Можете ли вы объяснить, как это сохранит состояние экземпляра фрагмента в заднем стеке? Это то, что спросил ОП.
Hitmaneidos
51
Это не связано с вопросом, onSaveInstance не вызывается, когда фрагмент помещается в backstack
Тадас Валайтис
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 в переменных и с помощьюView s только для их отображения, синхронизируя две вещи. Я не считаю последнюю часть очень чистой, хотя.

Ви
источник
68
Это лучшее решение, которое я нашел до сих пор, но осталась еще одна (несколько экзотическая) проблема: если у вас есть два фрагмента, Aи B, где Aв данный момент находится в backstack и Bвиден, вы теряете состояние A(невидимое 1) если повернуть дисплей дважды . Проблема в том, что onCreateView()не вызывается только в этом сценарии onCreate(). Итак, позже onSaveInstanceState()нет никаких представлений, от которых можно спасти государство. Нужно было бы сохранить, а затем сохранить переданное состояние onCreate().
devconsole
7
@devconsole Я хотел бы дать вам 5 голосов за этот комментарий! Это вращение дважды убивало меня в течение нескольких дней.
DroidT
Спасибо за отличный ответ! У меня есть один вопрос, хотя. Где лучше всего создать экземпляр объекта модели (POJO) в этом фрагменте?
Renjith
7
Чтобы сэкономить время для других, App.VSTUPи App.STAVявляются строковые метки, которые представляют собой объекты , они пытаются получить. Пример: savedState = savedInstanceState.getBundle(savedGamePlayString);илиsavedState.getDouble("averageTime")
Таннер Холлмен
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 это может быть как бы неявным образом выведено из документов. Например, документ beginTransaction () выглядит следующим образом: «Это потому, что инфраструктура заботится о сохранении ваших текущих фрагментов в состоянии (...)». Я также довольно давно кодирую свои приложения с этим ожидаемым поведением.
Рикардо
1
@Ricardo это применимо, если вы используете ViewPager?
Дерек Битти
1
Обычно да, если только вы не изменили поведение по умолчанию в своей реализации FragmentPagerAdapterили FragmentStatePagerAdapter. Если вы посмотрите на код FragmentStatePagerAdapter, например, вы увидите, что restoreState()метод восстанавливает фрагменты из того, что FragmentManagerвы передали в качестве параметра при создании адаптера.
Рикардо
4
Я думаю, что этот вклад является лучшим ответом на оригинальный вопрос. Это также тот, который, по моему мнению, лучше всего согласуется с тем, как работает платформа Android. Я бы рекомендовал пометить этот ответ как «Принятый», чтобы лучше помогать будущим читателям.
дБмВт
17

Я просто хочу дать решение, которое я придумал, которое обрабатывает все случаи, представленные в этом посте, которые я получил от Vasek и devconsole. Это решение также обрабатывает особый случай, когда телефон поворачивается более одного раза, а фрагменты не видны.

Здесь я храню пакет для дальнейшего использования, так как 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);
    }
}

Так как destroyView не вызывается в особой ситуации вращения, мы можем быть уверены, что если оно создает состояние, мы должны его использовать.

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

Теперь вот сложная часть. В моем методе onActivityCreated я создаю экземпляр переменной myObject, но вращение происходит по onActivity и onCreateView не вызывается. Поэтому myObject будет нулевым в этой ситуации, когда ориентация поворачивается более одного раза. Я обхожу это путем повторного использования того же пакета, который был сохранен в 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);
}

Теперь, где бы вы ни хотели восстановить состояние, просто используйте комплект saveState

  @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    ...
    if(savedState != null) {
        myObject = (MyObject) savedState.getSerializable(SAVED_BUNDLE_TAG);
    }
    ...
}
DroidT
источник
Можете ли вы сказать мне ... Что такое "MyObject" здесь?
Кави
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 в onActivityCreated:

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

    if (hasSavedState()) {
        return;
    }

    //your code here
}
Noturno
источник
-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
Не относится к исходному вопросу.
Хорхе Э. Эрнандес