首页 文章

一劳永逸,如何在后台堆栈中正确保存Fragments的实例状态?

提问于
浏览
407

我在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 类型的成员元素中(它实际上是更多的数据,而不仅仅是一个 TextView )并将其保存在 onSaveInstanceState() 中,但还有其他缺点 . 首先,如果当前显示片段,则调用这两个函数的顺序是相反的,因此我需要考虑两种不同的情况 . 必须有一个更清洁,更正确的解决方案!

6 回答

  • 15

    要正确保存 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.important point ,在活动中,您必须将片段的实例保存在 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);
    }
    

    希望这可以帮助 .

  • 75

    我只想提供我想出的解决方案来处理本文中我从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;
    }
    

    现在 here 是棘手的部分 . 在我的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);
        }
        ...
    }
    
  • -1

    这就是我现在使用的方式......它非常复杂,但至少它可以处理所有可能的情况 . 如果有人有兴趣 .

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

    Alternatively ,始终有可能将数据显示在被动 View 中的变量中,并仅使用 View 来显示它们,保持两者同步 . 不过,我不认为最后一部分很干净 .

  • 3

    在最新的支持库中,这里讨论的解决方案都不再需要 . 您可以使用 FragmentTransaction 随意播放 Activity 的片段 . 只需确保您的片段可以使用ID或标记进行标识 .

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

    这是一个例子:

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

    但请注意,当恢复片段的隐藏状态时,当前存在bug . 如果要在活动中隐藏片段,则需要在这种情况下手动恢复此状态 .

  • 48

    感谢DroidT,我做了这个:

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

    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
    }
    
  • 472
    final FragmentTransaction ft = getFragmentManager().beginTransaction();
    ft.hide(currentFragment);
    ft.add(R.id.content_frame, newFragment.newInstance(context), "Profile");
    ft.addToBackStack(null);
    ft.commit();
    

相关问题