保存界面状态

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

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

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

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

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

用户预期和系统行为

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

用户发起的界面状态解除

用户希望当他们导航到某个屏幕时,该屏幕的暂时性界面状态会保持不变,直到他们完全关闭该屏幕为止。用户可通过执行以下操作来完全关闭屏幕或应用:

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

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

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

系统发起的界面状态解除

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

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

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

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

用于保留界面状态的选项

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

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

ViewModel 保存的状态 永久性存储空间
存储位置 在内存中 在内存中 在磁盘或网络上
在配置更改后继续存在
在系统发起的进程终止后继续存在
在用户完全关闭屏幕/finish() 后继续存在
数据限制 支持复杂对象,但是空间受可用内存的限制 仅适用于基元类型和简单的小对象,例如 String 仅受限于磁盘空间或从网络资源检索的成本/时间
读取/写入时间 快(仅限内存访问) 慢(需要序列化/反序列化) 慢(需要磁盘访问或网络事务)

使用 ViewModel 处理配置更改

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

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

与已保存状态不同,ViewModel 在系统发起的进程终止过程中会被销毁。如需在 ViewModel 中由系统发起的进程终止结束后重新加载数据,请使用 SavedStateHandle API。或者,如果数据与界面相关,不需要存储在 ViewModel 中,请使用 rememberSerializable。对于原始数据类型或您不想使用 @Serializable 的场景,请使用 rememberSaveable。如果数据是应用数据,最好将其保存到磁盘。

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

使用已保存的状态作为备份来处理系统发起的进程终止

Compose 中的 rememberSerializablerememberSaveable 以及 ViewModel 中的 SavedStateHandle 等 API 会存储一些数据,以供系统在销毁组件后重新创建时,用于重新加载界面状态。为了更高效地处理复杂的数据结构,SavedStateHandle 通过 saved {} 扩展程序支持 Kotlinx 序列化,让您能够无缝地持久保存和恢复类型安全的对象以及标准基本类型。如需了解如何使用 rememberSaveable 实现已保存的状态,请参阅状态和 Jetpack Compose

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

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

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

此外,如果您从 intent 打开 activity,则当配置发生更改以及系统恢复该 activity 时,会将 extra 捆绑包传送给该 activity。在 activity 启动时,如果一段界面状态数据(例如搜索查询)作为 intent extra 传入,则您可以使用 extra 捆绑包而不是保存的状态捆绑包。如需详细了解 intent extra,请参阅 Intent 和 Intent 过滤器

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

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

使用 SavedStateRegistry 接入已保存状态

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

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

  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)
      }
  }

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

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

  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 SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

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

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

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

管理界面状态:分而治之

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

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

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

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

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

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

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

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

  • 在系统结束应用进程后,需要重新创建界面。系统已使用已保存状态 API 保存查询。ViewModel(使用 SavedStateHandle)或可组合函数(使用 rememberSerializablerememberSaveable)会自动恢复查询。如果可组合函数恢复了查询,则会将查询传递给 ViewModelViewModel 看到没有缓存任何搜索结果时,会委托使用指定的搜索查询加载搜索结果。
  • 在配置更改后重新创建界面。由于 ViewModel 实例尚未销毁,因此 ViewModel 会将所有信息缓存在内存中,而无需重新查询数据库。

其他资源

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

Codelab

查看内容