保存界面状态 (View)

概念和 Jetpack Compose 实现

本指南介绍了用户对界面状态的预期,以及可用于保持状态的选项。

在系统销毁 activity 或应用后,快速保存和恢复 activity 的界面状态对打造良好的用户体验至关重要。用户希望界面状态保持不变,但系统可能会销毁 activity 及其存储状态。

如需使系统行为符合用户预期,您可组合使用以下方法:

最佳解决方案取决于界面数据的复杂程度、应用的使用场景,以及数据访问速度与内存用量之间的平衡。

确保您的应用符合用户的预期,并提供快速的自适应界面。避免在将数据加载到界面时出现延迟,尤其是在发生常见配置更改(比如旋转)之后。

用户预期和系统行为

根据用户执行的操作,他们会希望系统清除或保留 activity 状态。在某些情况下,系统会自动执行用户预期的操作。但有时,系统会执行与用户预期相反的操作。

用户发起的界面状态解除

用户希望当他们启动 activity 时,该 activity 的暂时性界面状态会保持不变,直到用户完全关闭 activity 为止。用户可通过执行以下操作来完全关闭 activity:

  • 从“概览”(“最近使用的应用”)屏幕中滑动关闭 activity。
  • 从“设置”屏幕中终止或强制退出应用。
  • 重新启动设备。
  • 完成某种“完成”操作(由 Activity.finish() 提供支持)。

在这些完全关闭的情况下,用户会认为他们已经永久离开 activity,如果他们重新打开 activity,会希望 activity 以干净的状态启动。系统在这些关闭场景中的基础行为符合用户预期,即 activity 实例将连同其中存储的任何状态以及与该 activity 关联的任何已保存实例状态记录一起被销毁并从内存中移除。

这条关于完全关闭的规则有一些例外情况,例如用户可能希望浏览器转到他们之前使用返回按钮退出浏览器之前正在查看的确切网页。

系统发起的界面状态解除

用户期望 activity 的界面状态在整个配置更改(例如旋转或切换到多窗口模式)期间保持不变。但是,默认情况下,系统会在发生此类配置更改时销毁 activity,从而清除存储在 activity 实例中的任何界面状态。如需详细了解设备配置,请参阅 配置参考页面。

请注意,您可以替换针对配置更改的默认行为,但不建议这样做。如需了解详情,请参阅处理 配置更改。

如果用户暂时切换到其他应用,稍后再返回到您的应用,他们也会希望 activity 的界面状态保持不变。例如,用户在您的搜索 activity 中执行搜索,然后按主屏幕按钮或接听电话,当他们返回搜索 activity 时,希望看到搜索广告关键字和结果仍在原处,并和之前完全一样。

在这种情况下,您的应用会被置于后台,系统会尽最大努力将您的应用进程留在内存中。但是,当用户转而去与其他应用进行互动时,系统可能会销毁您的应用进程。在这种情况下,activity 实例连同其中存储的任何状态都会一起被销毁。当用户重新启动应用时,activity 会出乎意料地处于干净状态。 如需详细了解进程终止行为,请参阅进程和应用生命周期。

用于保留界面状态的选项

当用户对界面状态的预期与默认系统行为不符时,您需要保存并恢复用户的界面状态,以确保系统发起的销毁对用户完全透明。

按照以下几个会影响用户体验的维度考量,用于保持界面状态的每个选项都有所差异:

ViewModel

保存的实例状态

永久性存储空间

存储位置

在内存中

在内存中

在磁盘或网络上

在配置更改后继续存在

是

是

是

在系统发起的进程终止后继续存在

否

是

是

在用户完全关闭 activity 或触发 finish() 后继续存在

否

否

是

数据限制

支持复杂对象,但是空间受可用内存的限制

仅适用于基元类型和简单的小对象,例如 String

仅受限于磁盘空间或从网络资源检索的成本 / 时间

读取/写入时间

快(仅限内存访问)

慢(需要序列化/反序列化)

慢(需要磁盘访问或网络事务)

使用 ViewModel 处理配置更改

ViewModel 非常适合在用户正活跃地使用应用时存储和管理界面相关数据。它支持快速访问界面数据,并且有助于避免在发生旋转、窗口大小调整和其他常见的配置更改后从网络或磁盘中重新获取数据。如需了解如何实现 ViewModel,请参阅 ViewModel 指南。

ViewModel 将数据保留在内存中,这意味着开销要低于从磁盘或网络检索数据。ViewModel 与一个 activity(或其他某个生命周期所有者)相关联,在配置更改期间保留在内存中,系统会自动将 ViewModel 与发生配置更改后产生的新 activity 实例相关联。

当用户退出您的 activity 或 fragment 时,或者在您调用 finish() 的情况下,系统会自动销毁 ViewModel,这意味着状态会被清除,正如用户在这些场景中所预期的一样。

与保存的实例状态不同,ViewModel 在系统发起的进程终止过程中会被销毁。如需在 ViewModel 中由系统发起的进程终止结束后重新加载数据,请使用 SavedStateHandle API。或者,如果数据与界面相关,不需要存储在 ViewModel 中,请使用 onSaveInstanceState()。如果数据是应用数据,最好将其 保存到磁盘。

如果您已有用于在发生配置更改后存储界面状态的内存中解决方案,则可能不需要使用 ViewModel。

使用保存的实例状态作为后备方法来处理系统发起的进程终止

View 系统中的 onSaveInstanceState() 回调和 SavedStateHandle 在 ViewModel 中存储一些数据(如 activity 或 fragment),以供系统在销毁 界面控制器后重新创建时,用于重新加载界面控制器的状态。如需了解如何使用 onSaveInstanceState 实现保存的实例状态 ,请参阅activity 生命周期指南 中的“保存和恢复 activity 状态”。

保存的实例状态捆绑包在配置更改和进程终止后会保留下来,但会因不同的 API 将数据序列化,而受存储容量和速度的限制。如果序列化的对象很复杂,序列化会占用大量的内存。因为此过程在配置更改期间发生在主线程上,所以长时间运行的序列化可能会导致丢帧和视觉卡顿。

保存的实例状态不应用于存储大量数据(如位图),或需要冗长的序列化或反序列化操作的复杂数据结构。而是只能用于存储基元类型和简单的小对象,例如 String。因此,请使用保存的实例状态来存储最少量的必要数据(例如 ID),以便在其他保留机制失败时重新创建必要的数据,将界面恢复到以前的状态。大多数应用都应实现此机制来处理系统发起的进程终止。

根据应用的使用场景,您可能完全不需要使用保存的实例状态。例如,浏览器可能会将用户带回他们在退出浏览器之前正在查看的确切网页。如果 activity 表现出这种行为,您可以放弃使用保存的实例状态,改为在本地保留所有内容。

此外,如果您从 intent 打开 activity,则当配置发生更改以及系统恢复该 activity 时,会将 extra 捆绑包传送给该 activity。

在上述任一情况下,您仍然可以使用 ViewModel 来避免 因在配置更改期间从数据库重新加载数据而浪费周期时间。

如果要保留的是简单的轻量级界面数据,那么您可以单独使用保存的实例状态 API 来保留状态数据。

使用 SavedStateRegistry 接入已保存状态

从 Fragment 1.1.0 或其传递依赖项 Activity 1.0.0 开始,界面控制器(例如 Activity 或 Fragment)会实现 SavedStateRegistryOwner 并提供绑定到该控制器的 SavedStateRegistry。SavedStateRegistry 允许组件连接到界面控制器的已保存状态,以使用后者或向其提供内容。例如,ViewModel 的已保存状态模块使用 SavedStateRegistry 创建 SavedStateHandle 并将其提供给您的 ViewModel 对象。您可以通过调用 getSavedStateRegistry 从界面控制器中检索 SavedStateRegistry。

对已保存状态提供内容的组件必须实现 SavedStateRegistry.SavedStateProvider,后者定义了一个名为 saveState 的方法。saveState() 方法允许组件返回 Bundle,其中包含应从该组件保存的任何状态。SavedStateRegistry 在界面控制器生命周期的保存状态阶段调用此方法。

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

如需注册 SavedStateProvider,请对 SavedStateRegistry 调用 registerSavedStateProvider(),并传递一个与提供方的数据以及提供方相关联的密钥。对 SavedStateRegistry 调用 consumeRestoredStateForKey() 并传入与提供程序数据关联的密钥即可从已保存状态中检索之前为提供程序保存的数据。

在 Activity 或 Fragment 中,您可以在调用 super.onCreate() 后在 onCreate() 中注册 SavedStateProvider。或者,您也可以对 SavedStateRegistryOwner 设置用于实现 LifecycleOwner 的 LifecycleObserver,然后在 ON_CREATE 事件发生后立即注册 SavedStateProvider。通过使用 LifecycleObserver,您可以将先前保存状态的注册和检索从 SavedStateRegistryOwner 本身分离。

Kotlin

class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val PROVIDER = "search_manager"
        private const val QUERY = "query"
    }

    private val query: String? = null

    init {
        // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
        registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_CREATE) {
                val registry = registryOwner.savedStateRegistry

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this)

                // Get the previously saved state and restore it
                val state = registry.consumeRestoredStateForKey(PROVIDER)

                // Apply the previously saved state
                query = state?.getString(QUERY)
            }
        }
    }

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }

    ...
}

class SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

针对复杂或大型数据使用本地持久性存储来处理进程终止

只要您的应用安装在用户的设备上,持久性本地存储(例如数据库或共享偏好设置)就会继续存在(除非用户清除应用的数据)。虽然此类本地存储空间会在系统启动的活动和应用进程终止后继续存在,但由于必须从本地存储空间读取到内存,因此检索成本高昂。这种持久性本地存储空间通常已经是应用架构的一部分,用于存储您打开和关闭 activity 时不想丢失的所有数据。

ViewModel 和已保存实例状态均不是长期存储解决方案,因此不能替代本地存储空间,例如数据库。您只应该使用这些机制来暂时存储瞬时界面状态,对于其他应用数据,应使用永久性存储空间。请参阅应用架构指南,详细了解如何充分利用本地存储空间长期保留您的应用模型数据(例如在重启设备后)。

管理界面状态:分而治之

您可以通过在各种类型的保留机制之间划分工作,高效地保存和恢复界面状态。在大多数情况下,这些机制中的每一种都应存储 activity 中使用的不同类型的数据,具体取决于数据复杂度、访问速度和生命周期的权衡:

  • 本地持久性存储:存储在您打开和关闭 activity 时不希望丢失的所有应用数据。
    • 示例:歌曲对象的集合,其中可能包括音频文件和元数据。
  • ViewModel:将显示关联界面所需的所有数据(即屏幕界面状态)存储在内存中。
    • 示例:最近搜索的歌曲对象和最近的搜索查询。
  • 保存的实例状态:存储少量的数据,以便在系统停止界面后又重新创建时,用于轻松重新加载界面状态。这里不存储复杂对象,而是将复杂对象保留在本地存储空间中,并将这些对象的唯一 ID 存储在保存的实例状态 API 中。
    • 示例:存储最近的搜索查询。

例如,假设有一个用于搜索歌曲库的 activity。应按如下方式处理不同的事件:

当用户添加歌曲时,ViewModel 会立即委托在本地保留此数据。如果新添加的这首歌曲应显示在界面中,则您还应更新 ViewModel 对象中的数据以表明该歌曲已添加。切记要在主线程以外执行所有数据库插入操作。

当用户搜索歌曲时,从数据库加载的任何复杂歌曲数据都应作为屏幕界面状态的一部分立即存储在 ViewModel 对象中。

当 activity 进入后台且系统调用保存的实例状态 API 时,应将搜索查询存储在保存的实例状态中,以备进程重新创建时使用。由于加载在此过程中保留下来的应用数据需要用到搜索查询,因此应将其存储在 ViewModel SavedStateHandle 中。这些就是加载数据并让界面恢复到当前状态所需的所有信息。

恢复复杂的状态:重组碎片

当到了用户该返回 activity 的时候,重新创建 activity 存在两种可能情况:

  • 在系统停止 activity 后,需要重新创建该 activity。系统已将查询保存在保存的实例状态捆绑包中,如果未使用 SavedStateHandle,则界面应将查询传递给 ViewModel。ViewModel 看到没有缓存任何搜索结果时,会委托使用指定的搜索查询加载搜索结果。
  • 在配置更改后重新创建 activity。由于 ViewModel 实例尚未销毁,因此 ViewModel 会将所有信息缓存在内存中,而无需重新查询数据库。

其他资源

如需详细了解如何保存界面状态,请参阅以下资源。

博客

Codelab