zoukankan      html  css  js  c++  java
  • 保存和恢复 Android Fragment 的状态

    经过几年在 Android 应用开发中应用 Fragment 的努力之后,我必须要说尽管Fragment的概念非常优秀,但是它也同时带来了一堆问题。当我们处理实例的状态保存时就需要特别一件一件地修护好。

    首先,尽管已经有了一个类似 Activity 中的 onSaveInstanceState 方法,但是它显然不能覆盖所有情况。换种说法就是,你不能仅仅依赖于 onSaveInstanceState 方法来保存/恢复视图的状态。这里有一些案例研究。

    案例1:只有一个 Fragment 在栈中时,旋转屏幕

    案例1
    案例1

    屏幕旋转是用来测试实例状态的保存/恢复的最简单的案例。这种情况很容易处理,你仅仅需要简单地保存一些东西,比如:成员变量,它也会在屏幕旋转时在 onSaveInstanceState 丢失,在 onActivityCreated 或者 onViewStateRestored 中恢复,如下所示:

    int someVar;
    @Override
       protected void onSaveInstanceState(Bundle outState) {
       outState.putInt("someVar", someVar);
       outState.putString(“text”, tv1.getText().toString());
    }
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
       super.onActivityCreated(savedInstanceState);
       someVar = savedInstanceState.getInt("someVar", 0);
       tv1.setText(savedInstanceState.getString(“text”));
    }

    看上去是不是很好?不过也不是全不管用。这种情况是在 onSaveInstanceState 不被回调,但是视图重新生成。这意味着什么?UI 里的所有东西都没了。下面就是这种案例。

    案例2:后退栈(Back Stack)中的 Fragment

    案例2
    案例2

    当一个 fragment 从后退栈中返回时(Fragment A就是在这种情况),Fragment A 中视图将会遵循下图的 Fragment 生命周期被重新创造出来。

    Fragment 生命周期
    Fragment 生命周期

    你将会看到Fragment从后退栈中返回时,会回调 onDestroyview 方法和 onCreateview 方法。不管怎样,显然在这种情况 onSaveInstanceState 方法没有被调用。结果就是 UI 里的所有都没有了,然后默认按照 Layout XML 文件中定义的来重新创建。

    不管怎样,实现了内在视图状态保存的视图,如:带有 android:freeezeText 的 EditText 或者 TextView,仍然能够保存好视图的状态,这是因为 Fragment 实现了对内在视图的状态保存,但我们这些开发者不能抓住这些事件。我们唯一能做的就是在 onDestroyView 方法中手动保存实例状态。

    @Override
    public void onSaveInstanceState(Bundle outState) {
       super.onSaveInstanceState(outState);
       // Save State Here
    }
    @Override
    public void onDestroyView() {
       super.onDestroyView();
       // Save State Here
    }

    问题也随之而来,onDestroyView 不提供任何帮助来保存实例状态到一个 Bundle,那我们应该把这些实例状态保存到什么地方呢? 答案就是 Argument, 它会随着 Fragment 一直保存着。

    现在代码如下所示:

    Bundle savedState;
    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
       super.onActivityCreated(savedInstanceState);
       // Restore State Here
       if (!restoreStateFromArguments()) {
          // First Time running, Initialize something here
       }
    }
    @Override
    public void onSaveInstanceState(Bundle outState) {
       super.onSaveInstanceState(outState);
       // Save State Here
       saveStateToArguments();
    }
    @Override
    public void onDestroyView() {
       super.onDestroyView();
       // Save State Here
       saveStateToArguments();
    }
    private void saveStateToArguments() {
       savedState = saveState();
       if (savedState != null) {
          Bundle b = getArguments();
          b.putBundle(“internalSavedViewState8954201239547”, savedState);
       }
    }
    private boolean restoreStateFromArguments() {
       Bundle b = getArguments();
       savedState = b.getBundle(“internalSavedViewState8954201239547”);
       if (savedState != null) {
          restoreState();
          return true;
       }
       return false;
    }
    /////////////////////////////////
    // Restore Instance State Here
    /////////////////////////////////
    private void restoreState() {
       if (savedState != null) {
          // For Example
          //tv1.setText(savedState.getString(“text”));
       }
    }
    //////////////////////////////
    // Save Instance State Here
    //////////////////////////////
    private Bundle saveState() {
       Bundle state = new Bundle();
       // For Example
       //state.putString(“text”, tv1.getText().toString());
       return state;
    }

    你能够容易地在 saveState 保存你的 fragment 的状态,在 restoreState 恢复状态。现在已经看起来好多了不少。我们已经快结束了,但是还有一种怪异的情况。

    案例3:在后退栈中超过一个 fragment 时,旋转屏幕两次

    案例3
    案例3

    当你旋转屏幕一次,onSaveInstanceState 会被回调,正如我们所期待的,UI 的状态会被保存。但是当你旋转屏幕超过一次,上述的代码可能导致应用的崩溃。原因就是尽管当你旋转屏幕时, onSaveInstanceState 方法被调用,但是在后退栈中的 fragment 会完全销毁视图,直到你浏览返回到原来那个 fragment 才会重新创建。因此,你再次旋转屏幕,就没有视图来保存状态。当你试图访问那些不存在的视图,saveState() 将会导致 NullPointerException,从而使应用崩溃。

    方法就是检查在 fragment 中视图是否存在。如果存在那就保存,如果不存在,那就在 Argument 中 savedState 不需要保存,然后返回时保存。或者我们甚至不需要做任何事,因为在Argument 中已经做好了。

    private void saveStateToArguments() {
       if (getView() != null)
          savedState = saveState();
       if (savedState != null) {
          Bundle b = getArguments();
          b.putBundle(“savedState”, savedState);
       }
    }

    哈,现在全都解决了!

    Fragment 最终模版

    如下就是我现在用于我工作中的 fragment 模版。

    import android.os.Bundle;
    import android.support.v4.app.Fragment;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import com.inthecheesefactory.thecheeselibrary.R;
    
    /**
     *Created by nuuneoi on 11/16/2014.
     */
    public class StatedFragment extends Fragment {
    
        Bundle savedState;
    
        public StatedFragment() {
            super();
        }
    
        @Override
        public void onActivityCreated(Bundle savedInstanceState) {
            super.onActivityCreated(savedInstanceState);
            // Restore State Here
            if (!restoreStateFromArguments()) {
                // First Time, Initialize something here
                onFirstTimeLaunched();
            }
        }
    
        protected void onFirstTimeLaunched() {
    
        }
    
        @Override
        public void onSaveInstanceState(Bundle outState) {
            super.onSaveInstanceState(outState);
            // Save State Here
            saveStateToArguments();
        }
    
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            // Save State Here
            saveStateToArguments();
        }
    
        ////////////////////
        // Don't Touch !!
        ////////////////////
    
        private void saveStateToArguments() {
            if (getView() != null)
                savedState = saveState();
            if (savedState != null) {
                Bundle b = getArguments();
                b.putBundle("internalSavedViewState8954201239547", savedState);
            }
        }
    
        ////////////////////
        // Don't Touch !!
        ////////////////////
    
        private boolean restoreStateFromArguments() {
            Bundle b = getArguments();
            savedState = b.getBundle("internalSavedViewState8954201239547");
            if (savedState != null) {
                restoreState();
                return true;
            }
            return false;
        }
    
        /////////////////////////////////
        // Restore Instance State Here
        /////////////////////////////////
    
        private void restoreState() {
            if (savedState != null) {
                // For Example
                //tv1.setText(savedState.getString("text"));
                onRestoreState(savedState);
            }
        }
    
        protected void onRestoreState(Bundle savedInstanceState) {
    
        }
    
        //////////////////////////////
        // Save Instance State Here
        //////////////////////////////
    
        private Bundle saveState() {
            Bundle state = new Bundle();
            // For Example
            //state.putString("text", tv1.getText().toString());
            onSaveState(state);
            return state;
        }
    
        protected void onSaveState(Bundle outState) {
    
        }
    }

    如果你使用这个模版,仅仅只需简单地继承这个类StatedFragment,然后在 onSaveState()中保存事物,在 onRestoreState() 中恢复它们。上述代码就会为你做好剩下的工作,我相信这已经覆盖了我已知的可能情况。

    setRetainInstance 能够帮助开发者在布局改变时(如:屏幕旋转)处理成员变量 而你可能注意到我没有设置 setRetaionInstance 为 true。请记住,这就是我的目的,因为setRetainInstance(true)并没有覆盖全部的情况。最主要的原因是你不能一次又一次地保存那些经常背使用的嵌套 fragment 。所以我建议就是不要保存实例,除非你100%确定这个 fragment 不会用于嵌套。

    用法

    好消息。这个博客讲述的 StateFragment 现在加入了一个非常容易使用的库,现在已经在 jcenter 上发布了。现在你可以简单地在你工程的 build.gradle 文件中加上一个依赖。如下所示:

    dependencies {
        compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment-support-v4:0.9.1'
    }
    

      

    继承 StateFragment ,然后在 onSaveState(Bundle outState) 中保存状态,在 onRestoreState(Bundle saveInstanceState)中恢复状态。如果你想在 fragment 第一次启动时做点什么的话,你也可以覆盖 onFirstTimeLaunched() 方法(在之后不会被调用)。

    public class MainFragment extends StatedFragment {
    
        ...
    
        /**
         * Save Fragment's State here
         */
        @Override
        protected void onSaveState(Bundle outState) {
            super.onSaveState(outState);
            // For example:
            //outState.putString("text", tvSample.getText().toString());
        }
    
        /**
         * Restore Fragment's State here
         */
        @Override
        protected void onRestoreState(Bundle savedInstanceState) {
            super.onRestoreState(savedInstanceState);
            // For example:
            //tvSample.setText(savedInstanceState.getString("text"));
        }
    
        ...
    
    }

    任何的评论和建议都很欢迎!


    这是本人课余时间的翻译,也是我第一次使用markdown书写,错误很多,还请耐心指出,谢谢!
    原文链接请点击原文链接

  • 相关阅读:
    [CF1462F] The Treasure of The Segments
    [CF1466E] Apollo versus Pan
    SYZOJ 搭建 Note
    [CF1476D] Journey
    [CF1476E] Pattern Matching
    [CF1494D] Dogeforces
    [CF1383B] GameGame
    [CF1383A] String Transformation 1
    [CF1453D] Checkpoints
    [CF1453C] Triangles
  • 原文地址:https://www.cnblogs.com/findwg/p/4491445.html
Copyright © 2011-2022 走看看