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

5,352 阅读8分钟

译者亦枫注:对于 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 的类中。

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

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

欢迎关注我


本文由 亦枫 创作并首发于 亦枫的个人博客 ,同步推送个人微信公众号:技术鸟(NiaoTech),欢迎关注。