Подтвердить что ты не робот

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

Я нашел много случаев аналогичного вопроса по 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));
}

С Fragment это работает только в очень специфических ситуациях. В частности, ужасно ломается замена фрагмента, помещение его в задний стек, а затем вращение экрана, пока отображается новый фрагмент. Из того, что я понял, старый фрагмент не получает вызов onSaveInstanceState() при замене, но остается каким-то образом связанным с Activity, и этот метод вызывается позже, когда его View больше не существует, поэтому ищем любой из мой TextView приводит к NullPointerException.

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

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

О да: это/это.

4b9b3361

Ответ 1

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

1. В фрагменте сохранить состояние экземпляра, переопределив onSaveInstanceState() и восстановить в onActivityCreated():

@Override
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    ...
    if (savedInstanceState != null) {
        //Restore the fragment state here
    }
}
...
@Override
public void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    //Save the fragment state here
}

2. И важная точка, в действии вам нужно сохранить экземпляр фрагмента в onSaveInstanceState() и восстановить его в onCreate().

public void onCreate(Bundle savedInstanceState) {
    ...
    if (savedInstanceState != null) {
        //Restore the fragment instance
        mContent = getSupportFragmentManager().getFragment(savedInstanceState, "myFragmentName");
        ...
    }
    ...
}

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);

    //Save the fragment instance
    getSupportFragmentManager().putFragment(outState, "myFragmentName", mContent);
}

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

Ответ 2

Так я использую в данный момент... это очень сложно, но, по крайней мере, он справляется со всеми возможными ситуациями. В случае, если кто-то заинтересован.

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

    /* (...) */

}

Альтернативно всегда есть возможность сохранять данные в пассивном View в переменных и использовать View только для их отображения, сохраняя две вещи в синхронизации. Однако я не считаю последнюю часть очень чистой.

Ответ 3

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

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

Ответ 4

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

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

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

Ответ 5

Благодаря 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
}

Ответ 6

final FragmentTransaction ft = getFragmentManager().beginTransaction();
ft.hide(currentFragment);
ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
ft.addToBackStack(null);
ft.commit();