一劳永逸,如何正确保存Fragments的实例状态到后退栈?

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

我有不同的肖像和风景布局,我正在使用回栈,这阻止我使用setRetainState()和技巧使用configuration更改例程。

我在TextView中向用户显示了一些信息,这些信息并没有保存在默认处理程序中。 当单独使用活动编写我的应用程序时,以下运作良好:

 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 ,这只适用于非常特殊的情况。 具体来说,什么是可怕的更换碎片,把它放在后面的堆栈,然后旋转屏幕,而新的片段显示。 根据我的理解,旧的片段在被replace时不会接收到对onSaveInstanceState()的调用,而是保持以某种方式链接到Activity并且当它的View不再存在时调用此方法,所以查找我的任何TextView的结果变成一个NullPointerException

另外,我发现保留对TextViews的引用对于Fragment s来说并不是一个好主意,即使对于Activity来说也是如此。 在这种情况下, onSaveInstanceState()实际上保存了状态,但是如果在隐藏片段时旋转屏幕两次 ,问题就会重新出现,因为onCreateView()在新实例中不会被调用。

我想将onDestroyView()的状态保存到一些Bundletypes的类成员元素(它实际上是更多的数据,不只是一个TextView ),并保存在onSaveInstanceState()但还有其他的缺点。 主要是,如果片段当前显示,调用这两个函数的顺序是相反的,所以我需要考虑两种不同的情况。 必须有一个更清洁和正确的解决scheme!

要正确保存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); } 

希望这可以帮助。

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

 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显示的数据保存在variables中,并使用View仅用于显示它们,保持两者同步。 不过,我不认为最后一部分很干净。

在最新的支持库上,这里所讨论的解决scheme都不再需要了。 您可以使用FragmentTransaction玩你的ActivityFragmentTransaction 。 只要确保你的片段可以用一个id或标签来标识。

只要您不尝试在每次调用onCreate()重新创build片段,片段就会自动恢复。 相反,您应该检查savedInstanceState是否为空,并在此情况下查找对创build的片段的旧引用。

这里是一个例子:

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

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

我只是想给我提出的解决scheme,处理这个post中提到的所有情况,我从Vasek和devconsole派生出来的。 此解决scheme还可以处理手机旋转多次而碎片不可见时的特殊情况。

这里是我存储的捆绑供以后使用,因为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,我们可以肯定,如果它创build了状态,我们应该使用它。

 @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”variables,但旋转发生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); } ... } 

感谢DroidT ,我做了这个:

我意识到,如果Fragment不执行onCreateView(),它的视图没有实例化。 所以,如果返回堆栈中的片断没有创build它的视图,我保存最后存储的状态,否则我build立我自己的捆绑与我想要保存/恢复的数据。

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