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