(译) 保存 UI 状态

原文链接

在系统启动的 activity 或应用程序销毁过程中及时保留和恢复 activity 的 UI 状态是用户体验的关键部分。在这些情况下,用户期望 UI 状态保持不变,但系统会销毁 activity 以及存储在其中的任何状态。

为了弥合用户期望和系统行为之间的差距,使用 ViewModel 对象,onSaveInstanceState()方法和本地存储的组合来跨越此类应用程序和活动实例转换来保持 UI 状态。决定如何组合这些选项取决于 UI 数据的复杂性,应用程序的使用情况以及检索速度与内存使用情况的关系。

无论采用哪种方法,都应确保你的应用满足用户对其 UI 状态的期望,并提供流畅,快捷的UI(避免将数据加载到 UI 中的时间滞后,尤其是在频繁发生的配置更改后,例如旋转)。在大多数情况下,你应该同时使用 ViewModel 和 onSaveInstanceState()。

此文章讨论了用户对 UI 状态的期望,可用于保存状态的选项,各个的权衡和限制。

用户期望和系统行为

根据用户采取的操作,他们要么希望清除 activity 状态,要么保留 activity 状态。在某些情况下,系统会自动执行用户期望的操作。在其它情况下,系统与用户期望的相左。

用户启动的 UI 状态解除

用户期望在他们开始活动时,该 activity 的短时 UI 状态将保持不变,直到用户完全解除 activity 为止。用户可以通过以下方式完全解除 activity:

  • 按下后退按钮
  • 从概览(最近)页面上滑动 activity
  • 从 activity 中导航到上一页面
  • 从“设置”页面中删除该应用
  • 完成某种“完成” activity(由 Activity.finish()支持)

用户在这些完全解除的情形中的假设是他们已经永久地离开了 activity,如果他们重新打开 activity ,他们希望 activity 从干净状态开始。这些解除方案的基础系统行为与用户期望相匹配 - activity 实例以及存储在其中的任何状态以及与活动关联的任何已保存的实例状态记录将被销毁从内存中删除。

关于完全解除的规则有一些例外 - 例如,用户可能希望浏览器在使用后退按钮退出浏览器之前将它们带到他们正在查看的确切网页。

系统初始化的 UI 状态解除

用户期望活动的 UI 状态在整个配置更改期间保持不变,例如旋转或切换到多窗口模式。但是,默认情况下,系统会在发生此类配置更改时销毁活动,从而消除存储在活动实例中的所有 UI 状态。要了解有关设备配置的更多信息,请参阅配置参考页面。请注意,可以(但不建议)覆盖配置更改的默认行为。有关更多详细信息,请参阅处理配置更改自己

如果用户暂时切换到其他应用程序然后稍后返回你的应用程序,则用户还希望你的 activity 的UI 状态保持不变。例如,用户在你的搜索 activity 中执行搜索,然后按下主页按钮或接听电话 - 当他们返回搜索 activity 时,他们希望找到搜索关键字并且结果仍然存在,就像之前一样。

在这种情况下,你的应用程序将放置在后台,系统会尽最大努力将你的应用程序进程保留在内存中。但是,当用户离开与其他应用程序交互时,系统可能会破坏应用程序进程。在这种情况下,活动实例以及存储在其中的任何状态都将被销毁。当用户重新启动应用程序时,活动意外处于干净状态。要了解有关进程死亡的更多信息,请参阅进程和应用程序生命周期

保存 UI 状态 参考

当用户对 UI 状态的期望与默认系统行为不匹配时,你必须保存并恢复用户的 UI 状态,以确保系统启动的销毁对用户是透明的。

用于保留 UI 状态的每个选项都会随着影响用户体验的以下维度而变化:

视图模型 保存的实例状态 持久化存储
存储位置 内存 序列化到磁盘 磁盘或网络
配置发生变化时是否存活
系统初始化进程死亡时是否存活
用户完成 activity 移除或 onFinish() 时是否存活
数据限制 复杂对象可以处理,但是由于内存有限能放的东西也是有限的 只针对基本数据类型以及简单的小型对象例如String 只被磁盘空间或从网络资源获取数据的时间所限制
读写时间 很快(只是内存上的存取) 慢(需要序列化和反序列化以及磁盘存取) 慢(需要磁盘存取以及网络传输)

使用 ViewModel 来处理配置修改

ViewModel 是用户活跃使用应用程序时存储和管理 UI 相关数据的理想选择。它允许快速访问 UI 数据,并帮助你避免在旋转,窗口大小调整和其他常见配置更改中从网络或磁盘中重新获取数据。要了解如何实现 ViewModel,请参阅 ViewModel指南

ViewModel 将数据保留在内存中,这意味着它比磁盘或网络中的数据更轻量。 ViewModel 与 activity(或其它一些生命周期所有者)相关联 - 它在配置更改期间保留在内存中,系统自动将ViewModel 与配置更改产生的新活动实例相关联。

当用户退出 activity 或 fragment 或调用 finish() 时,系统会自动销毁 ViewModel,这意味着状态将在用户期望的情况下被清除。

与保存的实例状态不同,ViewModel 在系统启动的进程死亡期间被销毁。这就是为什么你应该将ViewModel 对象与onSaveInstanceState()或其他一些磁盘持久化)结合使用,在savedInstanceState 中存储标识符以帮助 ViewModel 在系统死亡后重新加载数据。

如果你已经有内存解决方案来存储跨配置更改的 UI 状态,则可能不需要使用 ViewModel。

使用 onSaveInstanceState() 作为备用方案来处理系统启动的进程死亡

如果系统销毁并稍后重新创建 例如 activity 或fragment 控制器, onSaveInstanceState() 回调存储重新加载 UI 控制器状态所需的数据。要了解如何实现已保存的实例状态,请参阅 activity 生命周期指南中的保存和恢复活动状态。

保存的实例状态包持续存在配置更改和进程死亡,但受存储量和速度的限制,因为onSavedInstanceState() 将数据序列化到磁盘。如果被序列化的对象很复杂,序列化会占用大量内存。由于此过程在配置更改期间在主线程上发生,因此序列化可能会导致丢帧和视觉卡顿(如果耗时太长)。

不要使用 store onSavedInstanceState() 来存储大量数据(如位图),也不要使用需要冗长序列化或反序列化的复杂数据结构。相反,只存储基本类型和简单的小对象,如 String。因此,如果其他持久性机制失败,则使用 onSaveInstanceState() 来存储必需的最少量数据(例如ID),以重新创建将 UI 还原回其先前状态所需的数据。大多数应用程序应该实现 onSaveInstanceState() 来处理系统启动的进程死亡。

根据应用程序的用例,你可能根本不需要使用 onSaveInstanceState()。例如,浏览器可能会在退出浏览器之前将用户带回他们正在查看的确切网页。如果你的 activity 以这种方式运行,你可以放弃使用 onSaveInstanceState() ,而是在本地保存所有内容。

此外,当你从 intent 打开 activity 时,会在配置更改和系统恢复 activity 时将附加bundle 包传递给 activity 。如果在启动 activity 时将一个UI状态数据(例如搜索查询)作为 intent extra 传递,则可以使用 extras bundle 而不是 onSaveInstanceState bundle。要了解有关 intent extras 的更多信息,请参阅 Intent 和 Intent Filters。

在上述任一情况下,你仍应使用 ViewModel 来避免在配置更改期间浪费从数据库重新加载数据的周期。

如果要保留的 UI 数据简单且轻量级,则可以单独使用 onSaveInstanceState() 来保留状态数据。

注意:你现在可以使用 ViewModel 的保存状态模块(当前为Alpha)提供对 ViewModel 对象中已保存状态的访问。可以通过名为 SavedStateHandle 的对象访问此已保存状态。你可以在Android 具有生命周期感知力的组件 codelab 中看到它的使用方式。

使用本地持久化来处理复杂或大型数据的进程死亡

只要你的应用程序安装在用户的设备上,持久化本地存储(例如数据库或 shared preferences)就会存在(除非用户清除应用程序的数据)。虽然这样的本地存储在系统启动的活动和应用程序进程死亡后仍然存在,但检索起来可能很昂贵,因为它必须从本地存储器读取到内存中。通常,此持久化本地存储可能已经是应用程序架构的一部分,用于存储您打开和关闭 activity 时不想丢失的所有数据。

ViewModel 和保存的实例状态都不是长期存储解决方案,因此不能替代本地存储,例如数据库。相反,你应该使用这些机制暂时存储临时 UI 状态,并将持久存储用于其他应用程序数据。有关如何利用本地存储长期保留应用程序模型数据(例如,跨设备重新启动)的详细信息,请参阅应用程序架构指南

管理UI状态:分而治之

通过在各种类型的持久性机制之间划分工作,你可以有效地保存和恢复 UI 状态。在大多数情况下,基于数据复杂性,访问速度和生命周期的权衡,这些机制中的每一个都应存储活动中使用的不同类型的数据:

  • 本地持久化:存储您打开和关闭活动时不想丢失的所有数据。
    示例:歌曲对象的集合,可包括音频文件和元数据。
  • ViewModel:在内存中存储显示关联UI控制器所需的所有数据。
    示例:最近搜索的歌曲对象和最近的搜索查询。
  • onSaveInstanceState():如果系统停止然后重新创建UI控制器,则存储轻松重新加载活动状态所需的少量数据。这里不是存储复杂对象,而是将复杂对象保存在本地存储中,并在onSaveInstanceState() 中存储这些对象的唯一 ID。
    • 示例:存储最新的搜索查询。

例如,考虑一个允许你搜索歌曲库的活动。以下是应该处理不同事件的方式:

当用户添加歌曲时,ViewModel 会立即委托在本地持久保存此数据。如果这个新添加的歌曲应该在UI 中显示,你还应该更新 ViewMode l对象中的数据以反映歌曲的添加。记得从主线程中删除所有数据库插件。

当用户搜索歌曲时,你从 UI 控制器的数据库加载的任何复杂歌曲数据应立即存储在 ViewModel 对象中。你还应该在 ViewModel 对象中保存搜索查询本身。

当 activity 进入后台时,系统调用 onSaveInstanceState()。你应该将搜索查询保存在onSaveInstanceState() bundle 中。这种少量数据很容易保存。它也是使活动恢复到当前状态所需的所有信息。

注意:最初创建 activity 时,onSaveInstanceState() bundle 不包含任何数据,并且ViewModel 对象为空。创建 ViewModel 对象时,传递一个空查询,该查询告诉 ViewModel 对象尚无加载数据。因此,activity 以空状态开始。

非典型前端coder wechat
想要随时Follow我的最新博客,可扫码关注我的公众号