[译]Android Activity 和 Fragment 状态保存与恢复的最佳实践

译者亦枫注:对于 Activity、Fragment 和 View 是如何保存与恢复状态的问题,相信很多开发人员都处于一知半解的状态。最近刚好在总结 Fragment 的使用注意事项,无意中从网上看到国外的一篇好文,对这个问题做了一个全面的解析。加之使用可视化的动画效果,使我们理解起来更加轻松。拜读过后,豁然开朗,同时不得不感慨,国外作者对于知识通透的理解能力和写作清晰的表达能力。然后,然后就一定要翻译过来,加以学习并保存记录之。

原文:The Real Best Practices to Save/Restore Activity's and Fragment's state. (StatedFragment is now deprecated)

作者:「nuuneoi」,一名拥有六年安卓应用程序开发经验和超过十二年手机端应用开发行业经验的全栈工程师。

几个月前我发表了一篇有关 Fragment 状态保存和恢复的文章:可能是目前为止保存和恢复 Fragment 状态的最佳方式(亦枫注:该文章已被删除,但 GitHub 上依然保有代码实现,可参考 StatedFragment。另外,我发现中外作者在标题设定上怎么套路都是一致的 _)。这篇文章收到了来自世界各地安卓开发人员的较有价值的反馈。非常感谢你们 =)

无论如何,StatedFragment 打破了常规设计模式,以一种不同的方式实现,就像 Android 设计 Fragment 之初就假定能够让安卓开发人员更容易理解 Fragment 的状态保存和恢复,如同 Activity 的做法一样(View 状态与 Instance 状态同时变迁)。所以我做了一个实验,开发出 StatedFragment 并看看到底能发展成怎样。是否更容易理解?这种模式是否更加利于开发?

此刻,经历了两个月的实践,我想我已经得到了结果。尽管 StatedFragment 理解起来稍微容易一些,但还是遇到了一个大问题。StatedFragment 打破了 Android View 架构的设计模式,所以我想这会导致一个长久的负面问题。事实上,我已经开始感觉到我的代码有些怪怪的了......

出于这个原因,我决定从现在开始废弃 StatedFragment。同时为了对这个错误的出现表示歉意,我写下这篇博文,向你们展示如何用 Android 的设计方式保存和恢复 Fragment 状态的最佳实践。

理解 Activity 状态保存和恢复时发生了什么


当 Activity 的 onSaveInstanceState 方法被调用时,Activity 会自动收集 View Hierachy(视图层次)中每一个 View 的状态。请注意,只有内部实现了 View 类状态保存和恢复方法的控件才能被收集状态数据。一旦 onRestoreInstanceState 方法被调用,Activity 将这些收集的数据回传给 View Hierachy 中的 View,而这种回传时数据与 View 一一对应关系的依据就是 View 提供之前保存数据时的相同 id,通常在布局中通过 android:id 属性定义的。

让我们通过可视化动画效果看一下:

Activity State Saving
Activity State Restoring

这就是为什么输入在 EditText 中的文本内容在 Activity 已经被销毁同时我们不用做任何事情的情况下依然能够保存的原因。这没什么不可思议的。这些 View 的状态会自动被收集和恢复回来。

同时这也是为什么那些没有定义 android:id 属性的 View 不能恢复状态的原因。

虽然这些 View 的状态可以被自动保存,但是 Activity 成员变量却不行。他们将随着 Activity 一起被销毁。你不得不通过 onSaveInstanceStateonRestoreInstanceState 方法手动保存和恢复这些成员变量。

public class MainActivity extends AppCompatActivity {

    // These variable are destroyed along with Activity
    private int someVarA;
    private String someVarB;

    ...

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("someVarA", someVarA);
        outState.putString("someVarB", someVarB);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        someVarA = savedInstanceState.getInt("someVarA");
        someVarB = savedInstanceState.getString("someVarB");
    }

}

这就是恢复 Activity Instance 状态和 View 状态你所需要做的事情。

Fragment 状态保存和恢复时发生了什么


假设 Fragment 被系统销毁,就会像 Activity 那样发生所有事情:

Fragment State Saving
Fragment State Restoring

也意味着每一个成员变量也被销毁。你不得不通过 onSaveInstanceStateonRestoreInstanceState 方法分别手动保存和恢复这些成员变量。但请注意,Fragment 类里面没有 onRestoreInstanceState 方法:

public class MainFragment extends Fragment {

    // These variable are destroyed along with Activity
    private int someVarA;
    private String someVarB;

    ...

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("someVarA", someVarA);
        outState.putString("someVarB", someVarB);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        someVarA = savedInstanceState.getInt("someVarA");
        someVarB = savedInstanceState.getString("someVarB");
    }

}

对于 Fragment,我认为你需要知道一些与 Activity 不同的地方。一旦 Fragment 从回退栈(BackStack)中返回时,View 将会被销毁和重建。

这种情况属于,Fragment 没有被销毁,但 Fragment 的 View 被销毁。因此,没有发生 Instance 状态保存。那么那些通过 Fragment 生命周期重新创建的 View 发生了什么呢?

不是问题。Android 是这么设计的。在这种情况下,View 状态保存和恢复在 Fragment 内部被调用。因此,每一个内部实现 View 类保存和恢复方法的 View,例如 EditText 或者 TextView,只要设置了 android:freezeText="true",都将被自动保存和恢复状态。数据和 View 的对应呈现关系和上面一样。

Fragment From BackStack

需要注意的是在这种情况下只有 View 被销毁和重建。Fragment 实例仍然在那儿,包括实例里的成员变量。所以你不需要对成员变量做任何事情。不需要额外添加任何代码:

public class MainFragment extends Fragment {

    // These variable still persist in this case
    private int someVarA;
    private String someVarB;

    ...

}

你可能已经注意到,如果 Fragment 中使用到的每一个 View 内部都实现了 View 类恢复和保存的方法,在这种情况下你就不需要做任何事情,因为 View 状态会自动恢复并且 Fragment 中的成员变量也仍然存在。

所以,有关 Fragment 状态保存和恢复最佳实践的第一个条件是...

你项目中用到的每一个 View 内部必须实现状态保存和恢复方法


Android 提供了一个通过 onSaveInstanceStateonRestoreInstanceState方法用于 View 内部保存和恢复状态的机制。开发人员在自定义 View 时实现这两个方法即可:

public class CustomView extends View {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}

基本上每一个单独的标准的 View 控件,如 EditTextTextViewCheckbox 等,都在内部实现了这些事情。而你所需要做的就是开启这个功能,比如你必须设置TextViewandroid:freezeText 属性值为 true 来使用这个功能。

但是如果是来自网上的第三方库里面的自定义 View 呢?我不得不说他们中的很多都没有实现这部分代码而导致我们在实际使用过程中出现很大的问题。

如果你决定使用第三方自定义 View,你必须保证这些 View 内部已经实现 View 状态保存和恢复,否则你必须创建一个子类继承自这些 View 并且自己实现 onSaveInstanceStateonRestoreInstanceState 方法。

//
// Assumes that SomeSmartButton is a 3rd Party view that
// View State Saving/Restoring are not implemented internally
//
public class SomeBetterSmartButton extends SomeSmartButton {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}

当然如果你创建了自己的自定义 View 或者自定义 ViewGroup ,不要忘了也要实现这两个方法。一定要记住项目中用到的每一种类型的 View 都要实现这部分代码。

同时也不要忘记分配 android:id 属性给 Layout 布局中你需要支持状态保存和恢复的每一个 View,否则这些 View 根本不会支持恢复状态。

    <EditText
        android:id="@+id/editText1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <EditText
        android:id="@+id/editText2"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

    <CheckBox
        android:id="@+id/cbAgree"
        android:text="I agree"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

到这里我们只进行到一半!

明确区分 Fragment 状态和 View 状态


为了使你的代码变得更加清晰和易于维护,你必须将 Fragment 状态和 View 状态区分开来。对于任何属于 View 的属性,在 View 内部实现状态保存和恢复。而对于那些属于 Fragment 的属性,就在 Fragment 内部实现即可。举个例子:

public class MainFragment extends Fragment {

    ...

    private String dataGotFromServer;
    
    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("dataGotFromServer", dataGotFromServer);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        dataGotFromServer = savedInstanceState.getString("dataGotFromServer");
    }

    ...

}

我再重复一遍,不要在 Fragment 的 onSaveInstanceState 方法中保存 View 状态,反之亦然。

StatedFragment


请按上面提及的方式保存和恢复 Activity、Fragment 和 View 的状态。现在让我将 StatedFragment 标记废除。

然而 StatedFragment 在嵌套 Fragment 中获取 onActivityResult 的功能使用起来仍然不错。为了避免将来产生疑惑,我决定从 v0.10.0 版本开始将这个功能单独拆分到一个新的命名为 NestedActivityResultFragment 的类中。

有关它的更多信息都在网址 https://github.com/nuuneoi/StatedFragment,请随时自由查阅。

希望这篇博文中的可视化动画能够帮助你清晰地理解 Activity 、Fragment 和 View 恢复状态的方式。另外对于之前文章造成的困惑表示歉意。>_<

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容