本指南讨论了用户对 UI 状态的期望,以及用于保留状态的可用选项。
在系统销毁活动或应用程序后快速保存和恢复活动的 UI 状态对于良好的用户体验至关重要。用户期望 UI 状态保持不变,但系统可能会销毁活动及其存储的状态。
为了弥合用户期望和系统行为之间的差距,请结合使用以下方法
ViewModel
对象。- 在以下上下文中保存实例状态
- Jetpack Compose:
rememberSaveable
。 - 视图:
onSaveInstanceState()
API。 - 视图模型:
SavedStateHandle
。
- Jetpack Compose:
- 本地存储以在应用和活动转换期间持久保存 UI 状态。
最佳解决方案取决于 UI 数据的复杂性、应用的用例以及在数据访问速度和内存使用之间找到平衡。
确保您的应用满足用户的期望并提供快速、响应灵敏的界面。避免在将数据加载到 UI 时出现延迟,尤其是在常见的配置更改(如旋转)之后。
用户期望和系统行为
根据用户采取的操作,他们要么期望活动状态被清除,要么期望状态被保留。在某些情况下,系统会自动执行用户期望的操作。在其他情况下,系统会执行与用户期望相反的操作。
用户启动的 UI 状态关闭
用户期望在他们启动活动时,该活动的瞬态 UI 状态保持不变,直到用户完全关闭该活动为止。用户可以通过执行以下操作完全关闭活动
- 从概览(最近使用)屏幕上滑动关闭活动。
- 从“设置”屏幕中杀死或强制退出应用。
- 重启设备。
- 完成某种“完成”操作(由
Activity.finish()
支持)。
用户在这些完全关闭情况下的假设是,他们已永久导航离开该活动,如果他们重新打开该活动,他们期望该活动从干净状态开始。这些关闭场景的基本系统行为与用户期望相匹配 - 活动实例将被销毁并从内存中移除,以及其中存储的任何状态以及与该活动关联的任何保存的实例状态记录。
此关于完全关闭的规则有一些例外 - 例如,用户可能期望浏览器将他们带到他们使用后退按钮退出浏览器之前正在查看的网页。
系统启动的 UI 状态关闭
用户期望活动的 UI 状态在配置更改(如旋转或切换到多窗口模式)期间保持不变。但是,默认情况下,系统在发生此类配置更改时会销毁活动,从而清除存储在活动实例中的任何 UI 状态。要了解有关设备配置的更多信息,请参阅 配置参考页面。请注意,可以(尽管不建议)覆盖配置更改的默认行为。有关更多详细信息,请参阅 自己处理配置更改。
用户还期望,即使他们暂时切换到其他应用然后再返回您的应用,您的 Activity 的 UI 状态也保持不变。例如,用户在您的搜索 Activity 中执行搜索,然后按下主页按钮或接听电话 - 当他们返回搜索 Activity 时,他们期望找到搜索关键字和结果仍然存在,与之前完全一样。
在这种情况下,您的应用会被置于后台,系统会尽力将您的应用进程保留在内存中。但是,当用户离开与其他应用交互时,系统可能会销毁应用程序进程。在这种情况下,Activity 实例将被销毁,以及存储在其中的任何状态。当用户重新启动应用时,Activity 意外地处于干净状态。要详细了解进程死亡,请参阅进程和应用程序生命周期。
保留 UI 状态的选项
当用户对 UI 状态的期望与默认系统行为不符时,您必须保存和恢复用户 UI 状态,以确保系统发起的销毁对用户来说是透明的。
保留 UI 状态的每个选项都沿以下影响用户体验的维度变化
ViewModel | 已保存的实例状态 | 持久性存储 | |
---|---|---|---|
存储位置 | 内存中 | 内存中 | 磁盘或网络上 |
在配置更改后继续存在 | 是 | 是 | 是 |
在系统发起的进程死亡后继续存在 | 否 | 是 | 是 |
在用户完全关闭 Activity/onFinish() 后继续存在 | 否 | 否 | 是 |
数据限制 | 复杂对象没问题,但空间受可用内存限制 | 仅适用于基本类型和简单的小对象,例如 String | 仅受磁盘空间或从网络资源检索的成本/时间限制 |
读写时间 | 快速(仅内存访问) | 缓慢(需要序列化/反序列化) | 缓慢(需要磁盘访问或网络事务) |
使用 ViewModel 处理配置更改
ViewModel 非常适合在用户积极使用应用程序时存储和管理与 UI 相关的数据。它允许快速访问 UI 数据,并帮助您避免在旋转、窗口大小调整和其他常见配置更改中从网络或磁盘重新获取数据。要了解如何实现 ViewModel,请参阅ViewModel 指南。
ViewModel 将数据保留在内存中,这意味着它比从磁盘或网络检索数据更便宜。ViewModel 与 Activity(或其他一些生命周期所有者)相关联 - 它在配置更改期间保留在内存中,并且系统会自动将 ViewModel 与配置更改导致的新 Activity 实例关联。
当您的用户退出您的 Activity 或 Fragment 或如果您调用finish()
时,系统会自动销毁 ViewModel,这意味着在这些情况下,状态会按用户预期清除。
与已保存的实例状态不同,ViewModel 在系统发起的进程死亡期间会被销毁。要在 ViewModel 中系统发起的进程死亡后重新加载数据,请使用SavedStateHandle
API。或者,如果数据与 UI 相关并且不需要保存在 ViewModel 中,请在 View 系统中使用onSaveInstanceState()
或在 Jetpack Compose 中使用rememberSaveable
。如果数据是应用程序数据,则最好将其持久保存到磁盘。
如果您已经有一个用于在配置更改中存储 UI 状态的内存解决方案,则可能不需要使用 ViewModel。
使用已保存的实例状态作为备份来处理系统发起的进程死亡
View 系统中的onSaveInstanceState()
回调,Jetpack Compose 中的rememberSaveable
以及 ViewModel 中的SavedStateHandle
存储重新加载 UI 控制器(例如 Activity 或 Fragment)状态所需的数据,如果系统销毁并随后重新创建该控制器。要了解如何使用onSaveInstanceState
实现已保存的实例状态,请参阅Activity 生命周期指南中的保存和恢复 Activity 状态。
已保存的实例状态捆绑包在配置更改和进程死亡期间都保持持久性,但受存储和速度限制,因为不同的 API 会序列化数据。如果要序列化的对象很复杂,序列化可能会消耗大量内存。由于此过程在配置更改期间发生在主线程上,因此长时间运行的序列化会导致帧丢失和视觉卡顿。
不要使用已保存的实例状态来存储大量数据,例如位图,也不要存储需要长时间序列化或反序列化的复杂数据结构。相反,只存储基本类型和简单的小对象,例如String
。因此,使用已保存的实例状态来存储必要的最小数据量,例如 ID,以重新创建恢复 UI 到其先前状态所需的数据,以防其他持久性机制失败。大多数应用都应该实现这一点来处理系统发起的进程死亡。
根据您应用的使用案例,您可能根本不需要使用已保存的实例状态。例如,浏览器可能会将用户带回到他们退出浏览器之前正在查看的网页。如果您的 Activity 以这种方式运行,您可以放弃使用已保存的实例状态,而是将所有内容本地持久化。
此外,当您从 Intent 打开 Activity 时,无论配置发生更改还是系统恢复 Activity,额外内容的捆绑包都会传递到 Activity。如果 UI 状态数据(例如搜索查询)在启动 Activity 时作为 Intent 额外内容传递,则可以使用额外内容捆绑包而不是已保存的实例状态捆绑包。要详细了解 Intent 额外内容,请参阅Intent 和 Intent 过滤器。
在这两种情况下,您都应该使用ViewModel
以避免在配置更改期间浪费周期从数据库重新加载数据。
在需要保留的 UI 数据简单且轻量级的情况下,您可以单独使用已保存的实例状态 API 来保留您的状态数据。
使用 SavedStateRegistry 挂钩到已保存的状态
从Fragment 1.1.0或其传递依赖项Activity 1.0.0开始,UI 控制器(例如Activity
或Fragment
)实现SavedStateRegistryOwner
并提供一个SavedStateRegistry
,该注册表绑定到该控制器。SavedStateRegistry
允许组件挂钩到您的 UI 控制器的已保存状态以使用或参与其中。例如,ViewModel 的已保存状态模块使用SavedStateRegistry
创建SavedStateHandle
并将其提供给您的ViewModel
对象。您可以通过调用getSavedStateRegistry()
从 UI 控制器内部检索SavedStateRegistry
。
参与已保存状态的组件必须实现SavedStateRegistry.SavedStateProvider
,它定义了一个名为saveState()
的单个方法。saveState()
方法允许您的组件返回一个包含应从该组件保存的任何状态的Bundle
。SavedStateRegistry
在 UI 控制器的生命周期保存状态阶段调用此方法。
Kotlin
class SearchManager : SavedStateRegistry.SavedStateProvider { companion object { private const val QUERY = "query" } private val query: String? = null ... override fun saveState(): Bundle { return bundleOf(QUERY to query) } }
Java
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
。或者,您可以在实现 LifecycleOwner
的 SavedStateRegistryOwner
上设置一个 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); ... }
使用本地持久化来处理复杂或大型数据的进程死亡
持久性本地存储(例如数据库或共享首选项)将在您的应用程序安装在用户设备上的时间内一直存在(除非用户清除应用程序的数据)。虽然此类本地存储可以在系统启动的活动和应用程序进程死亡后继续存在,但检索起来可能代价高昂,因为它必须从本地存储读取到内存中。通常,此持久性本地存储可能已经是应用程序架构的一部分,用于存储在打开和关闭活动时不想丢失的所有数据。
ViewModel 和保存的实例状态都不是长期存储解决方案,因此不能替代本地存储(例如数据库)。相反,您应该使用这些机制仅临时存储瞬态 UI 状态,并将持久性存储用于其他应用程序数据。有关如何利用本地存储长期(例如跨设备重启)持久化应用程序模型数据的更多详细信息,请参阅 应用程序架构指南。
管理 UI 状态:分而治之
您可以通过在各种类型的持久化机制之间分配工作来有效地保存和恢复 UI 状态。在大多数情况下,这些机制中的每一个都应根据数据复杂性、访问速度和生命周期的权衡,存储活动中使用的不同类型的数据。
- 本地持久化:存储在打开和关闭活动时不想丢失的所有应用程序数据。
- 示例:歌曲对象的集合,其中可能包括音频文件和元数据。
ViewModel
:在内存中存储显示关联 UI 所需的所有数据,即 屏幕 UI 状态。- 示例:最近搜索的歌曲对象和最近的搜索查询。
- 保存的实例状态:存储少量数据,如果系统停止然后重新创建 UI,则需要这些数据来重新加载 UI 状态。不要在此处存储复杂的对象,而是将复杂的对象持久化到本地存储中,并在保存的实例状态 API 中存储这些对象的唯一 ID。
- 示例:存储最近的搜索查询。
例如,考虑一个允许您搜索歌曲库的活动。以下是不同事件的处理方式
当用户添加歌曲时,ViewModel
会立即委托本地持久化此数据。如果应在 UI 中显示此新添加的歌曲,则还应更新 ViewModel
对象中的数据以反映歌曲的添加。请记住,所有数据库插入操作都应在主线程之外执行。
当用户搜索歌曲时,无论您从数据库加载什么复杂的歌曲数据,都应将其作为屏幕 UI 状态的一部分立即存储在 ViewModel
对象中。
当活动进入后台并且系统调用保存的实例状态 API 时,应将搜索查询存储在保存的实例状态中,以防进程重新创建。由于此信息对于加载在此持久化的应用程序数据是必要的,因此请将搜索查询存储在 ViewModel 的 SavedStateHandle
中。这是您加载数据并将 UI 恢复到其当前状态所需的所有信息。
恢复复杂状态:重新组合各个部分
当用户需要返回到活动时,重新创建活动有两种可能的情况
- 活动在被系统停止后重新创建。系统已将查询保存在保存的实例状态捆绑包中,如果未使用
SavedStateHandle
,则 UI 应将查询传递给ViewModel
。ViewModel
发现它没有缓存搜索结果,并委托使用给定的搜索查询加载搜索结果。 - 活动在配置更改后创建。由于
ViewModel
实例尚未销毁,因此ViewModel
将所有信息缓存到内存中,无需重新查询数据库。
其他资源
要了解有关保存 UI 状态的更多信息,请参阅以下资源。
博客
为您推荐
- 注意:当 JavaScript 关闭时,会显示链接文本
- ViewModel的Saved State模块
- 使用生命周期感知组件处理生命周期
- ViewModel 概述