在 Jetpack Compose 中,可组合函数通常使用 remember 函数来保存状态。如状态和 Jetpack Compose 中所述,记住的值可在重组时重复使用。
虽然 remember 可作为在重组期间保持值的工具,但状态通常需要在组合的生命周期之外存在。本页介绍了 remember、retain、rememberSaveable 和 rememberSerializable API 之间的区别、何时选择哪个 API,以及在 Compose 中管理记忆值和保留值的最佳实践。
选择正确的使用寿命
在 Compose 中,您可以使用多个函数来跨组合及其他场景持久保存状态:remember、retain、rememberSaveable 和 rememberSerializable。这些函数的生命周期和语义各不相同,并且各自适合存储特定类型的状态。下表概述了这些区别:
|
|
|
|
|---|---|---|---|
值在重组后是否仍然存在? |
✅ |
✅ |
✅ |
值在 activity 重新创建后是否会继续留存? |
❌ |
✅ 始终返回相同的 ( |
✅ 系统将返回一个等效 ( |
值在进程终止后是否继续存在? |
❌ |
❌ |
✅ |
支持的数据类型 |
全部 |
不得引用在销毁 activity 时会泄露的任何对象 |
必须可序列化 |
用例 |
|
|
|
remember
remember 是在 Compose 中存储状态的最常见方式。当首次调用 remember 时,系统会执行给定的计算并记住结果,这意味着 Compose 会存储该结果,以便可组合函数日后重复使用。当可组合函数重组时,它会再次执行其代码,但对 remember 的任何调用都会从之前的组合中返回其值,而不是再次执行计算。
可组合函数的每个实例都有一组自己的记忆值,称为位置记忆化。当记忆的值被记忆化以供在重组中使用时,它们会与在组合层次结构中的位置相关联。如果某个可组合函数在不同位置使用,则组合层次结构中的每个实例都有一组自己的记忆值。
当不再使用记忆的值时,系统会忘记该值并舍弃其记录。当记住的值从组合层次结构中移除时(包括在不使用 key 可组合项或 MovableContent 的情况下移除值并重新添加以移至其他位置时),或者使用不同的 key 参数调用时,系统会忘记这些值。
在可用的选项中,remember 的生命周期最短,并且会最早忘记本页中介绍的四种记忆化函数的值。因此,它最适合用于:
- 创建内部状态对象,例如滚动位置或动画状态
- 避免在每次重组时重新创建开销较大的对象
不过,您应避免:
- 使用
remember存储任何用户输入,因为在 activity 配置更改和系统发起的进程终止后,系统会忘记已记忆的对象。
rememberSaveable 和 rememberSerializable
rememberSaveable 和 rememberSerializable 基于 remember 构建。它们是本指南中讨论的记忆化函数中生命周期最长的。除了在重组后按位置记忆对象之外,它还可以保存值,以便在重新创建 activity 时(包括因配置更改和进程终止而重新创建 activity 时)恢复这些值(当系统在您的应用位于后台时终止其进程时,通常是为了释放内存以供前台应用使用,或者用户在您的应用运行时撤消其权限时)。
rememberSerializable 的工作方式与 rememberSaveable 相同,但会自动支持使用 kotlinx.serialization 库可序列化的复杂类型的持久性。如果您的类型标记为 @Serializable(或可以标记为 @Serializable),请选择 rememberSerializable;在所有其他情况下,请选择 rememberSaveable。
因此,rememberSaveable 和 rememberSerializable 非常适合存储与用户输入相关联的状态,包括文本字段条目、滚动位置、切换状态等。您应保存此状态,以确保用户永远不会丢失其位置。一般来说,您应使用 rememberSaveable 或 rememberSerializable 来记忆应用无法从其他持久性数据源(例如数据库)检索的任何状态。
请注意,rememberSaveable 和 rememberSerializable 通过将记忆化值序列化为 Bundle 来保存这些值。这会带来以下两个影响:
- 您记忆的值必须能以以下一种或多种数据类型表示:基元(包括
Int、Long、Float、Double)、String或上述任一类型的数组。 - 恢复已保存的值时,该值将是一个与之前组合使用的新实例(
==),但不是相同的引用(===)。
如需存储更复杂的数据类型而不使用 kotlinx.serialization,您可以实现自定义 Saver,以将对象序列化和反序列化为支持的数据类型。请注意,Compose 可开箱即用地理解 State、List、Map、Set 等常见数据类型,并自动将这些类型转换为受支持的类型。以下是 Size 类的 Saver 示例。它通过使用 listSaver 将 Size 的所有属性打包到列表中来实现。
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
就记忆值的时间长短而言,retain API 介于 remember 和 rememberSaveable/rememberSerializable 之间。之所以命名不同,是因为保留的值的生命周期也不同于记忆的值。
当某个值被保留时,它会被位置记忆化,并保存在具有单独生命周期的辅助数据结构中,该生命周期与应用的生命周期相关联。保留的值能够在配置更改后继续存在,而无需序列化,但无法在进程终止后继续存在。如果某个值在重新创建组合层次结构后未使用,则保留的值会停用(相当于 retain 被遗忘)。
为了实现这种短于 rememberSaveable 的生命周期,retain 能够持久保存无法序列化的值,例如 lambda 表达式、flow 和大型对象(如位图)。例如,您可以使用 retain 管理媒体播放器(例如 ExoPlayer),以防止在配置更改期间中断媒体播放。
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain对ViewModel
从核心来看,retain 和 ViewModel 在其最常用的功能(即在配置更改期间持久保留对象实例)方面提供类似的功能。选择使用 retain 还是 ViewModel 取决于您要持久保存的值的类型、其作用域应如何设置,以及您是否需要其他功能。
ViewModel 是通常封装应用界面层和数据层之间通信的对象。它们可让您将逻辑移出可组合函数,从而提高可测试性。ViewModel 在 ViewModelStore 中作为单例进行管理,并且具有不同于保留值的生命周期。ViewModel 会一直保持有效状态,直到其 ViewModelStore 被销毁,而保留的值会在内容从组合中永久移除时失效(例如,对于配置更改,这意味着如果重新创建了界面层次结构,并且在重新创建组合后未消耗保留的值,则该保留的值会失效)。
ViewModel 还包括与 Dagger 和 Hilt 进行依赖项注入的开箱即用集成、与 SavedState 的集成,以及用于启动后台任务的内置协程支持。因此,ViewModel 非常适合启动后台任务和网络请求、与项目中的其他数据源互动,以及选择性地捕获和持久保存关键的界面状态,这些状态应在 ViewModel 中跨配置变更保留,并在进程终止后继续存在。
retain 最适合作用域限定为特定可组合实例的对象,并且不需要在同级可组合项之间重复使用或共享。ViewModel 非常适合用于存储界面状态和执行后台任务,而 retain 则非常适合用于存储界面管道的对象,例如缓存、展示跟踪和分析、对 AndroidView 的依赖项,以及与 Android 操作系统交互或管理第三方库(例如付款处理方或广告)的其他对象。
对于设计现代 Android 应用架构建议之外的自定义应用架构模式的高级用户:retain 还可用于构建内部“ViewModel 类似”API。虽然 retain 不提供对协程和已保存状态的开箱即用支持,但它可以作为此类 ViewModel 类似物的生命周期构建块,在这些类似物之上构建这些功能。有关如何设计此类组件的具体细节不在本指南的讨论范围内。
|
|
|
|---|---|---|
确定范围 |
没有共享值;每个值都保留在合成层次结构的特定点,并与该点相关联。在不同位置保留相同类型的实例始终会作用于新实例。 |
|
销毁 |
永久离开组合层次结构时 |
|
其他功能 |
可以在对象位于组合层次结构中或不在组合层次结构中时接收回调 |
内置 |
所有者 |
|
|
用例 |
|
|
组合 retain 和 rememberSaveable 或 rememberSerializable
有时,对象需要同时具有 retained 和 rememberSaveable 或 rememberSerializable 的混合生命周期。这可能表明您的对象应该是 ViewModel,它可以支持已保存状态,如 ViewModel 的已保存状态模块指南中所述。
可以同时使用 retain 和 rememberSaveable 或 rememberSerializable。正确地组合这两个生命周期会增加相当大的复杂性。
我们建议将此模式用作更高级别和自定义架构模式的一部分,并且仅在以下所有条件都成立时使用:
- 您要定义一个由必须保留或保存的值组成的混合对象(例如,跟踪用户输入的对象和无法写入磁盘的内存中缓存)
- 您的状态的作用域限定为可组合项,不适合
ViewModel的单例作用域或生命周期
如果上述所有情况都成立,我们建议将类拆分为三个部分:已保存的数据、保留的数据,以及一个没有自身状态的“中介”对象,该对象会委托给保留的对象和已保存的对象,以相应地更新状态。此模式采用以下形状:
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
通过按生命周期分离状态,职责和存储的分离变得非常明确。有意让保留数据无法操纵保存数据,这是为了防止在 savedInstanceState 软件包已被捕获且无法更新时尝试更新保存数据。它还允许您通过测试构造函数来测试重新创建场景,而无需调用 Compose 或模拟 Activity 重新创建。
如需查看此模式的完整实现示例,请参阅完整示例 (RetainAndSaveSample.kt)。
位置记忆化和自适应布局
Android 应用可以支持多种设备类型,包括手机、可折叠设备、平板电脑和桌面设备。应用经常需要使用自适应布局在这些设备规格之间转换。例如,在平板电脑上运行的应用可能能够显示双列列表详情视图,但在较小的手机屏幕上显示时,可能需要在列表和详情页面之间进行导航。
由于记忆的值和保留的值是按位置记忆的,因此只有当它们出现在组合层次结构中的相同位置时,才能重复使用。当布局适应不同的设备规格时,可能会改变组合层次结构的结构,导致值被遗忘。
对于 ListDetailPaneScaffold 和 NavDisplay(来自 Jetpack Navigation 3)等开箱即用型组件,这不会造成问题,您的状态将在整个布局更改过程中保持不变。对于可适应不同设备规格的自定义组件,请执行以下操作之一,确保状态不受布局更改的影响:
- 确保有状态的可组合项始终在组合层次结构中的同一位置调用。通过更改布局逻辑(而不是在组合层次结构中重新定位对象)来实现自适应布局。
- 使用
MovableContent可优雅地重新定位有状态可组合项。MovableContent的实例能够将记忆值和保留值从旧位置移到新位置。
记住工厂函数
虽然 Compose 界面由可组合函数组成,但在创建和组织组合时,需要用到许多对象。最常见的示例是定义了自己的状态的复杂可组合对象,例如接受 LazyListState 的 LazyList。
定义以 Compose 为中心的对象时,我们建议创建一个 remember 函数来定义预期的记忆行为,包括生命周期和键输入。这样一来,状态的使用者就可以放心地在组合层次结构中创建实例,这些实例将按预期保留并失效。定义可组合的工厂函数时,请遵循以下准则:
- 为函数名称添加前缀
remember。或者,如果函数实现依赖于对象是retained,并且 API 永远不会演变为依赖于remember的其他变体,请改用retain前缀。 - 如果选择了状态持久性,并且可以编写正确的
Saver实现,请使用rememberSaveable或rememberSerializable。 - 避免产生副作用或根据可能与使用情况无关的
CompositionLocal初始化值。请注意,创建状态的位置可能与使用状态的位置不同。
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }