remember VS rememberSaveable
引言
在 Jetpack Compose 中,我们常需要「记住」某个计算结果,避免在重组时重复计算以提升性能。Compose 提供了两种常用方式:remember {} 和 rememberSaveable {}。二者都能在重组时复用值,但持久化范围不同,选错会导致状态在特定场景下被重置。本文结合一次实际问题和文档/实验,说明两者的区别与使用场景。
一、问题现象
页面中有两个主要部分:Dashboard(根据滚动做缩放动画)和 Asset List(可滚动列表)。用 remember {} 保存 scale 状态时:
- 快速切换 Tab:表现正常,scale 保持。
- 在其他 Tab 停留一段时间再切回:scale 被重置,Dashboard 突然放大。

改为 rememberSaveable {} 后,无论如何切换 Tab,返回时 Dashboard 都能保持之前的 scale,符合预期。
相关代码片段
// 使用 rememberSaveable 保持 scale,避免离开屏幕后再进入时被重置
var scale by rememberSaveable {
mutableFloatStateOf(1.0f)
}
val nestedScrollConnection = remember { ... }
Box(modifier = modifier.nestedScroll(nestedScrollConnection)) {
DashboardView(
modifier = Modifier
.fillMaxWidth()
.scale(scale)
.graphicsLayer { translationY = toolbarHeightRange.last * (scale - 1.0f) },
onEvent = onEvent
)
LazyColumn(
modifier = Modifier
.fillMaxSize()
.graphicsLayer { translationY = toolbarState.height + toolbarState.offset }
.background(MaterialTheme.colorScheme.background)
.shadow(elevation = 3.dp, shape = RoundedCornerShape(topEnd = 20.dp, topStart = 20.dp))
.pointerInput(Unit) {
detectTapGestures(onPress = { scope.coroutineContext.cancelChildren() })
},
verticalArrangement = Arrangement.spacedBy(12.dp),
state = listState
) {
items(uiState.tokens, key = { it.token.id }) {
TokenItemView(extraToken = it, onEvent = onEvent)
}
}
}
二、原理解析
2.1 可组合项的生命周期
根据官方文档 Lifecycle of composables,可组合项的生命周期可分为三阶段:
- 进入组合(Enter Composition)
- 重组(Recomposition)
- 退出组合(Exit Composition)
离开当前页面(如切换 Tab)时,该页面的可组合项会退出组合;再次进入时则会重新进入组合,相当于新一轮生命周期。
2.2 remember {}
对 remember {} 的文档说明大致如下:
Remember the value produced by calculation. calculation will only be evaluated during the composition. Recomposition will always return the value produced by composition.
即:值在进入组合时由 block 计算得到,重组时直接返回该值,不会重新计算。
因此:
- 初次进入组合:执行 block,计算并保存值。
- 重组:直接返回已保存的值,不执行 block。
- 退出组合后再次进入:视为新的组合,block 会再次执行,状态会被重新初始化。
这就是「在其他 Tab 停留一段时间再切回」时 scale 被重置的原因:页面先退出组合,再进入时 remember { mutableFloatStateOf(1.0f) } 重新执行,scale 又变回 1.0。若切换很快,组合尚未被回收,就不会触发重新计算,看起来就「正常」。
2.3 rememberSaveable {}
官方对 rememberSaveable 的说明:
与
remember {}类似,但存储的值会在 Activity 或进程重建后 仍然保留,基于 Android 的SavedInstanceState机制。
要点:
默认使用
autoSaver()将值写入 Bundle;也可自定义Saver。仅支持可放入 Bundle 的类型(如 Int、String、Float 等)。
若保存的是自定义类型(如 data class),必须提供
Saver实现,否则会报错:java.lang.IllegalArgumentException: MutableState containing ... cannot be saved using the current SaveableStateRegistry. The default implementation only supports types which can be stored inside the Bundle.
因此,离开屏幕再进入时,只要没有发生 Activity/进程重建,rememberSaveable 仍会从 SaveableStateRegistry 里恢复之前保存的值,scale 得以保留。
三、验证实验
用简单日志验证「退出组合后再进入会重新计算」:
var scale by remember {
println("=== re-calculate")
mutableFloatStateOf(1.0f)
}
DisposableEffect(key1 = null) {
onDispose { println("=== onDispose") }
}
DisposableEffect(key1 = null) 保证只有在退出组合时才会执行 onDispose。
- **使用
remember {}**:第一次进入打印re-calculate;切到其他 Tab 停留后,先打印onDispose,再切回来会再次打印re-calculate,scale 被重置。
- **改用
rememberSaveable {}**:只有第一次进入时打印一次re-calculate,之后切 Tab 再回来也不会再打印,说明一直用的是已保存的值。
四、延伸:离开屏幕 ≠ 重建
Jetpack Compose 相关讨论 里有一句:**”Composable leaving the screen is not recreation”**(可组合项离开屏幕并不等于重建)。
在 Compose 中:
- 组件离开屏幕会触发
onDispose,生命周期结束。 - 再次进入屏幕会重新组合,相当于新的生命周期,
remember {}会重新计算。
所以「离开屏幕」和「Configuration 变化导致 Activity 重建」是两回事:前者不会触发 SavedInstanceState 流程,只会让可组合项退出组合;后者才会触发进程/Activity 重建,此时才用到 rememberSaveable 的持久化。
常见「离开屏幕」场景包括:
- Navigation 跳转 / Tab 切换(如本文案例)
- 列表滑动:Item 滑出视口时会退出组合,
onDispose会执行
例如 LazyColumn 的 Item 配合 DisposableEffect 可以观察到滑出时打印;若需要跨页面或重新进入页面时恢复列表滚动位置,应使用基于 rememberSaveable 的 rememberLazyListState 等 API。
五、如何选择
| 需求 | 推荐 |
|---|---|
| 状态只需在重组过程中保持 | remember {} |
| 状态需要在 Activity/进程重建(如旋转、内存回收)后恢复 | rememberSaveable {} |
| 状态需要跨页面或更复杂的作用域 | ViewModel 等;若仅在单页内且需应对「离开再进入」,rememberSaveable {} 仍适用 |
理解「进入/重组/退出组合」与「SavedInstanceState 重建」的差异后,就能在开发中按场景选用 remember 或 rememberSaveable,避免状态被意外重置。