本指南讨论了用户对 UI 状态的期望,以及可用于保留状态的选项。
在系统销毁活动或应用后,快速保存和恢复活动的 UI 状态对于良好的用户体验至关重要。用户期望 UI 状态保持不变,但系统可能会销毁活动及其存储的状态。
为了弥合用户期望与系统行为之间的差距,请结合使用以下方法:
ViewModel
对象。- 以下上下文中的已保存实例状态:
- Jetpack Compose:
rememberSaveable
。 - 视图:
onSaveInstanceState()
API。 - ViewModel:
SavedStateHandle
。
- Jetpack Compose:
- 本地存储,用于在应用和活动转换期间持久保留 UI 状态。
最佳解决方案取决于您的 UI 数据的复杂性、您的应用用例,以及在数据访问速度和内存使用之间找到平衡。
确保您的应用符合用户期望并提供快速、响应迅速的界面。避免在将数据加载到 UI 时出现延迟,尤其是在旋转等常见配置更改之后。
用户期望和系统行为
根据用户的操作,他们可能期望活动状态被清除或保留。在某些情况下,系统会自动执行用户期望的操作。在其他情况下,系统会执行与用户期望相反的操作。
用户发起的 UI 状态关闭
用户期望,当他们启动一个活动时,该活动的瞬态 UI 状态保持不变,直到用户完全关闭该活动。用户可以通过以下方式完全关闭一个活动:
- 从概览(最近使用)屏幕滑动关闭活动。
- 从“设置”屏幕终止或强制退出应用。
- 重新启动设备。
- 完成某种“完成”操作(由
Activity.finish()
提供支持)。
在这些完全关闭的情况下,用户的假设是他们已永久离开活动,如果他们重新打开活动,他们期望活动从全新状态开始。这些关闭场景的底层系统行为与用户期望相符 - 活动实例将被销毁并从内存中移除,同时移除其中存储的任何状态以及与活动关联的任何已保存实例状态记录。
此关于完全关闭的规则有一些例外——例如,用户可能期望浏览器将他们带到他们在使用返回按钮退出浏览器之前正在查看的精确网页。
系统发起的 UI 状态关闭
用户期望活动的 UI 状态在配置更改(例如旋转或切换到多窗口模式)期间保持不变。但是,默认情况下,当发生此类配置更改时,系统会销毁活动,从而清除活动实例中存储的任何 UI 状态。要了解有关设备配置的更多信息,请参阅配置参考页面。请注意,可以(但不建议)覆盖配置更改的默认行为。有关更多详细信息,请参阅自行处理配置更改。
如果用户暂时切换到其他应用,然后又返回到您的应用,他们也期望您的活动的 UI 状态保持不变。例如,用户在您的搜索活动中执行搜索,然后按下主屏幕按钮或接听电话 - 当他们返回搜索活动时,他们期望搜索关键字和结果仍然存在,与之前完全相同。
在这种情况下,您的应用会被置于后台,系统会尽力将您的应用进程保留在内存中。但是,当用户离开并与其他应用交互时,系统可能会销毁应用进程。在这种情况下,活动实例会被销毁,同时销毁其中存储的任何状态。当用户重新启动应用时,活动会意外地处于全新状态。要了解有关进程终止的更多信息,请参阅进程和应用生命周期。
保留 UI 状态的选项
当用户对 UI 状态的期望与默认系统行为不匹配时,您必须保存和恢复用户的 UI 状态,以确保系统发起的销毁对用户是透明的。
保留 UI 状态的每个选项在以下维度上有所不同,这些维度会影响用户体验:
ViewModel | 已保存实例状态 | 持久存储 | |
---|---|---|---|
存储位置 | 内存中 | 内存中 | 磁盘或网络上 |
经受住配置更改 | 是 | 是 | 是 |
经受住系统发起的进程终止 | 否 | 是 | 是 |
经受住用户完全关闭活动/onFinish() | 否 | 否 | 是 |
数据限制 | 复杂对象没问题,但空间受可用内存限制 | 仅适用于基元类型和简单、小型对象,如 String | 仅受磁盘空间或从网络资源检索的成本/时间限制 |
读/写时间 | 快(仅内存访问) | 慢(需要序列化/反序列化) | 慢(需要磁盘访问或网络事务) |
使用 ViewModel 处理配置更改
ViewModel 是在用户积极使用应用程序时存储和管理 UI 相关数据的理想选择。它允许快速访问 UI 数据,并帮助您避免在旋转、窗口大小调整以及其他常见配置更改期间从网络或磁盘重新获取数据。要了解如何实现 ViewModel,请参阅 ViewModel 指南。
ViewModel 将数据保留在内存中,这意味着它比从磁盘或网络检索数据更便宜。ViewModel 与活动(或其他生命周期所有者)关联 - 它在配置更改期间保留在内存中,并且系统会自动将 ViewModel 与配置更改产生的新活动实例关联起来。
当用户退出您的活动或片段,或者您调用 finish()
时,ViewModel 会被系统自动销毁,这意味着在这种情况下,状态会像用户期望的那样被清除。
与已保存实例状态不同,ViewModel 在系统发起的进程终止期间会被销毁。要在系统发起的进程终止后在 ViewModel 中重新加载数据,请使用 SavedStateHandle
API。或者,如果数据与 UI 相关且不需要保留在 ViewModel 中,请在 View 系统中使用 onSaveInstanceState()
或在 Jetpack Compose 中使用 rememberSaveable
。如果数据是应用数据,则最好将其持久保存到磁盘。
如果您已经有一个内存中解决方案用于在配置更改时存储 UI 状态,则可能不需要使用 ViewModel。
使用已保存实例状态作为备份来处理系统发起的进程终止
视图系统中的 onSaveInstanceState()
回调、Jetpack Compose 中的 rememberSaveable
以及 ViewModel 中的 SavedStateHandle
存储了在系统销毁并随后重新创建 UI 控制器(例如活动或片段)时重新加载其状态所需的数据。要了解如何使用 onSaveInstanceState
实现已保存实例状态,请参阅活动生命周期指南中的“保存和恢复活动状态”。
已保存实例状态捆绑包通过配置更改和进程终止都得以保留,但受存储和速度的限制,因为不同的 API 会序列化数据。如果序列化的对象很复杂,序列化可能会消耗大量内存。由于此过程在配置更改期间在主线程上发生,长时间运行的序列化可能会导致丢帧和视觉卡顿。
不要使用已保存实例状态来存储大量数据(例如位图)或需要长时间序列化或反序列化的复杂数据结构。相反,只存储基元类型和简单、小型对象(例如 String
)。因此,请使用已保存实例状态来存储所需的少量数据(例如 ID),以便在其他持久性机制失败时重新创建恢复 UI 到其先前状态所需的数据。大多数应用都应实现此功能以处理系统发起的进程终止。
根据您的应用用例,您可能完全不需要使用已保存实例状态。例如,浏览器可能会将用户带回到他们退出浏览器之前正在查看的精确网页。如果您的活动以这种方式运行,您可以放弃使用已保存实例状态,而是将所有内容持久化到本地。
此外,当您从 intent 打开活动时, extras 捆绑包会在配置更改时和系统恢复活动时传递给活动。如果 UI 状态数据(例如搜索查询)在活动启动时作为 intent extra 传入,则可以使用 extras 捆绑包而不是已保存实例状态捆绑包。要了解有关 intent extras 的更多信息,请参阅Intent 和 Intent 过滤器。
在这两种情况下,您仍应使用 ViewModel
,以避免在配置更改期间浪费周期从数据库重新加载数据。
在要保留的 UI 数据简单轻量的情况下,您可能单独使用已保存实例状态 API 来保留您的状态数据。
使用 SavedStateRegistry 挂钩到已保存状态
从 Fragment 1.1.0 或其传递依赖项 Activity 1.0.0 开始,UI 控制器(例如 Activity
或 Fragment
)实现 SavedStateRegistryOwner
并提供一个绑定到该控制器的 SavedStateRegistry
。SavedStateRegistry
允许组件挂钩到您的 UI 控制器的已保存状态,以使用或贡献它。例如,ViewModel 的 Saved State 模块使用 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 恢复到当前状态所需的所有信息。
恢复复杂状态:重新组合碎片
当用户需要返回活动时,重新创建活动有两种可能的情况:
- 活动在被系统停止后重新创建。系统在已保存实例状态 Bundle 中保存了查询,如果未使用
SavedStateHandle
,UI 应该将查询传递给ViewModel
。ViewModel
发现它没有缓存的搜索结果,并委托使用给定的搜索查询加载搜索结果。 - 活动在配置更改后创建。由于
ViewModel
实例未被销毁,ViewModel
在内存中缓存了所有信息,不需要重新查询数据库。
其他资源
要了解有关保存 UI 状态的更多信息,请参阅以下资源。
博客
为您推荐
- 注意:禁用 JavaScript 时显示链接文本
- ViewModel 的 Saved State 模块
- 使用生命周期感知组件处理生命周期
- ViewModel 概览