it-swarm.com.ru

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

Я обнаружил много случаев аналогичного вопроса в 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 больше не существует, поэтому поиск любого из моих TextViews приводит к NullPointerException ,.

Кроме того, я обнаружил, что сохранение ссылки на мое TextViews не является хорошей идеей с Fragments, даже если это было нормально с Activity. В этом случае onSaveInstanceState() фактически сохраняет состояние, но проблема появляется снова, если я поворачиваю экран дважды, когда фрагмент скрыт, так как его onCreateView() не вызывается в новом экземпляре.

Я думал о сохранении состояния в onDestroyView() в некоторый элемент-член класса типа Bundle (на самом деле это больше данных, а не просто одно TextView) и сохранение that в onSaveInstanceState(), но есть и другие недостатки. Прежде всего, если фрагмент есть, показанный в данный момент, порядок вызова двух функций меняется на обратный, поэтому мне нужно учитывать две разные ситуации. Должно быть более чистое и правильное решение!

440
The Vee

Чтобы правильно сохранить состояние экземпляра Fragment, вы должны сделать следующее:

1. Во фрагменте сохраните состояние экземпляра, переопределив onSaveInstanceState() и восстановив в 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. И важный момент, в упражнении вы должны сохранить экземпляр фрагмента в onSaveInstanceState() и восстановить в 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);
}

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

502
ThanhHH

Это способ, которым я сейчас пользуюсь ... он очень сложный, но, по крайней мере, он обрабатывает все возможные ситуации. На случай, если кому-то интересно.

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 только для их отображения, поддерживая синхронизацию двух вещей. Я не считаю последнюю часть очень чистой, хотя.

79
The Vee

В последней библиотеке поддержки ни одно из обсуждавшихся здесь решений больше не требуется. Вы можете играть с фрагментами своего 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);
    }
...
}

Однако обратите внимание, что в настоящее время существует ошибка при восстановлении скрытого состояния фрагмента. Если вы скрываете фрагменты в своей деятельности, в этом случае вам нужно будет восстановить это состояние вручную.

52
Ricardo Lage

Я просто хочу дать решение, которое я придумал, которое обрабатывает все случаи, представленные в этом посте, которые я получил от 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);
    }
    ...
}
16
DroidT

Благодаря DroidT , я сделал это:

Я понимаю, что если Fragment не выполняется 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
}
3
Noturno