remember VS rememberSaveable
引言
在 Jetpack Compose 中,remember {} 和 rememberSaveable {} 都能把值保留在组合期间,但它们解决的问题不同:前者把值留在当前 Composition 中;后者会把可保存的值交给 SaveableStateRegistry,以便在 Activity 或进程重建后恢复。选错保存边界,状态就可能在意料之外被重置。本文结合一次实际问题和文档/实验,说明两者的区别与使用场景。
一、问题现象
页面中有两个主要部分: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.
rememberSaveable 不是“只要离开页面再回来就一定能恢复”的开关。它能否在导航或 Tab 切换后找回值,取决于承载它的 SaveableStateRegistry 是否仍被保留,例如 Navigation Compose 的 back stack 或显式使用 SaveableStateHolder 的场景。
如果切换 Tab 时直接把页面从 Composition 移除,且没有保留对应的 saveable state,rememberSaveable 同样可能重新初始化;如果状态本来就应跨多个页面长期存在,则更适合放到 ViewModel(必要时结合 SavedStateHandle)中。
因此,本文案例的准确结论是:当 Tab 容器保留了页面对应的 saveable state 时,rememberSaveable 可以恢复 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 中,组件离开屏幕与 Activity 重建是两回事:
- 组件退出 Composition 后,
remember保存的值会丢失。 rememberSaveable的值会参与 Android 的状态保存与恢复;Activity 或进程重建后,系统有机会恢复它。- 页面离开后能否在同一进程内恢复
rememberSaveable,还取决于导航/Tab 容器是否保留对应的SaveableStateRegistry。
因此,不能把“离开屏幕”直接等同于“必然触发 rememberSaveable 恢复”。如果状态需要跨页面共享或具有业务意义,应提升到 ViewModel 等更合适的作用域。
常见「离开屏幕」场景包括:
- Navigation 跳转 / Tab 切换(如本文案例)
- 列表滑动:Item 滑出视口时会退出组合,
onDispose会执行
例如 LazyColumn 的 Item 配合 DisposableEffect 可以观察到滑出时打印;若需要跨页面或重新进入页面时恢复列表滚动位置,应使用基于 rememberSaveable 的 rememberLazyListState 等 API。
五、如何选择
| 需求 | 推荐 |
|---|---|
| 状态只需在重组过程中保持 | remember {} |
| 状态需要在 Activity/进程重建(如旋转、内存回收)后恢复 | rememberSaveable {} |
| 状态需要跨页面或更复杂的作用域 | ViewModel 等;若仅在单页内且需应对「离开再进入」,rememberSaveable {} 仍适用 |
理解「进入/重组/退出组合」与「SavedInstanceState 重建」的差异后,就能在开发中按场景选用 remember 或 rememberSaveable,避免状态被意外重置。