在 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 时恢复这些值,包括在配置更改和进程终止时(当系统在您的应用在后台运行时终止其进程时,通常是为了为前台应用释放内存,或者如果用户在您的应用运行时撤消了对该应用的权限)。
rememberSerializable 的工作方式与 rememberSaveable 相同,但会自动支持保留可使用 kotlinx.serialization 库序列化的复杂类型。如果您的类型已标记(或可以标记)为 @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 表达式、流和大型对象(例如位图)。例如,您可以使用 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 OS 互动或管理第三方库(例如支付处理方或广告)的其他对象。
对于在 Modern Android 应用架构建议之外设计自定义应用架构模式的高级用户:retain 还可用于构建内部“类似 ViewModel”的 API。虽然 retain 不提供对协程和已保存状态的开箱即用支持,但它可以作为此类类似 ViewModel 的生命周期的构建块,这些功能构建在其之上。如何设计此类组件的具体细节不在本指南的讨论范围内。
|
|
|
|---|---|---|
作用域 |
没有共享值;每个值都保留在组合层次结构中的 a 特定点并与该点相关联。在 不同位置保留相同类型始终会作用于新实例。 |
|
销毁 |
永久离开组合层次结构时 |
清除或销毁 |
其他功能 |
当对象位于组合层次结构中 或不在其中时,可以接收回调 |
内置 |
所有者 |
|
|
使用场景 |
|
|
组合 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 界面由可组合函数组成,但许多对象都参与了组合的创建和组织。最常见的示例是定义自己状态的复杂可组合对象,例如 LazyList,
它接受 LazyListState。
在定义以 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) } ) }