如何正确保存实例状态的碎片在后面的堆栈?

我在SO上发现了许多类似问题的实例,但不幸的是没有答案符合我的要求。

我有不同的纵向和横向布局,我使用背堆栈,这都阻止我使用setRetainState()和使用配置更改例程的技巧。

我在TextViews中显示某些信息给用户,这些信息不会保存在默认处理程序中。当我只使用Activities编写应用程序时,以下工作得很好:

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,这只在非常特定的情况下有效。具体来说,最糟糕的是替换一个片段,将其放入back堆栈,然后在显示新片段时旋转屏幕。据我所知,旧片段在被替换时不会接收到对onSaveInstanceState()的调用,但以某种方式保持与Activity的链接,当它的View不再存在时,此方法稍后被调用,因此寻找任何我的# eyz4结果为NullPointerException

此外,我发现保持我的TextViewsFragments的引用不是一个好主意,即使它对Activity的是OK的。在这种情况下,onSaveInstanceState()实际上保存了状态,但如果我在片段隐藏时旋转屏幕两次,问题就会再次出现,因为它的onCreateView()在新实例中没有被调用。

我想过将onDestroyView()中的状态保存到一些# eyz1类型的类成员元素中(它实际上是更多的数据,而不仅仅是一个TextView),并将保存在onSaveInstanceState()中,但还有其他缺点。首先,如果片段当前显示,则调用两个函数的顺序颠倒了,因此我需要考虑两种不同的情况。一定会有更干净和正确的解决方案!

363781 次浏览

这是一个非常古老的答案。

我不再为Android写东西了,所以最新版本的功能不能保证,也不会有任何更新。

这就是我现在使用的方法……它非常复杂,但至少它处理了所有可能的情况。如果有人感兴趣的话。

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仅用于显示它们,保持两者同步。不过,我觉得最后一部分不太干净。

要正确保存Fragment的实例状态,您应该执行以下操作:

在片段中,通过覆盖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);
}


}

我只是想给出我提出的解决方案,处理本文中提出的所有情况,这些情况都是我从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);
}

现在无论你想在哪里恢复状态,只要使用savedState包

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

在最新的支持库中,这里讨论的解决方案都不再是必需的。你可以使用FragmentTransaction来玩Activity的片段。只需确保您的片段可以使用id或标记进行标识。

只要您不尝试在每次调用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);
}
...
}

但是请注意,当恢复片段的隐藏状态时,当前有一个错误。如果您在活动中隐藏片段,在这种情况下,您将需要手动恢复此状态。

多亏了DroidT,我做出了这个:

我意识到如果Fragment不执行createview(),它的视图就不会被实例化。因此,如果后堆栈上的片段没有创建它的视图,我保存最后存储的状态,否则我用我想保存/恢复的数据构建自己的包。

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)例如,你可以在onActivityCreated中调用hasSavedState:

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


if (hasSavedState()) {
return;
}


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