Jetpack Compose 中 remember 和 rememberSaveable 的区别:一次 Tab 切换状态丢失排查
在 Jetpack Compose 中,remember {} 和 rememberSaveable {} 都能“记住”状态。
但它们的保存范围并不一样。
这个差异在简单页面里不明显,一旦遇到 Tab 切换、页面离开后重新进入、Activity 重建等场景,就很容易出现状态被意外重置的问题。
这篇文章结合一次真实排查,说明:
remember 只保证当前组合生命周期内的状态复用;rememberSaveable 适合需要在离开页面后仍然恢复的可保存状态。
一、问题现象
页面里有两个主要部分:
- Dashboard:根据滚动做缩放动画
- Asset List:可滚动列表
最开始使用 remember {} 保存 scale 状态时,表现并不稳定:
- 快速切换 Tab:看起来正常,
scale还能保持 - 在其他 Tab 停留一段时间再切回:
scale被重置,Dashboard 突然放大

改成 rememberSaveable {} 后,无论如何切换 Tab,返回时 Dashboard 都能保持之前的缩放状态。
相关代码如下:
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)
}
}
}
二、先理解组合生命周期
根据 Compose 官方文档,Composable 的生命周期可以简单分为三段:
- Enter Composition:进入组合
- Recomposition:重组
- Exit Composition:退出组合
当页面因为 Tab 切换、导航或列表滑出等原因离开当前 UI 树时,对应 Composable 可能会退出组合。
再次回到页面时,它不是简单“继续刚才的状态”,而是重新进入组合。
这正是 remember 和 rememberSaveable 差异出现的地方。
三、remember {} 保存的是什么
remember {} 的核心行为是:
在当前组合中记住计算结果,重组时复用这个值。
也就是说:
- 初次进入组合:执行 block,计算并保存值
- 普通重组:直接返回保存的值,不重新执行 block
- 退出组合后再次进入:进入新的组合生命周期,block 会重新执行
所以在这次问题中,如果页面离开 Tab 后被移出组合,再回来时:
remember { mutableFloatStateOf(1.0f) }
会重新执行,scale 自然回到 1.0f。
快速切换时之所以“看起来正常”,只是因为组合可能还没有被回收,并不代表 remember 能跨页面稳定保存状态。
四、rememberSaveable {} 多做了什么
rememberSaveable {} 和 remember {} 类似,也会在组合中保存状态。
不同的是,它会把可保存的值接入 Compose 的保存机制,基于 SaveableStateRegistry / Android SavedInstanceState 进行恢复。
它的要点是:
- 默认使用
autoSaver()保存 Bundle 支持的类型 - 支持
Int、String、Float等基础类型 - 自定义类型需要提供
Saver - 可以应对 Activity 重建、进程恢复,以及部分离开后重新进入的状态恢复场景
如果保存的是不支持自动保存的类型,通常会遇到类似错误:
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.
在本文这个场景里,scale 是 Float,属于可保存类型,因此适合用 rememberSaveable 保存。
五、用日志验证
可以用下面这段代码验证 remember 在退出组合后会重新计算:
var scale by remember {
println("=== re-calculate")
mutableFloatStateOf(1.0f)
}
DisposableEffect(key1 = null) {
onDispose { println("=== onDispose") }
}
DisposableEffect 的 onDispose 会在退出组合时执行。
使用 remember {} 时:
- 第一次进入页面:打印
re-calculate - 切到其他 Tab 停留后:打印
onDispose - 再切回来:再次打印
re-calculate,状态被重置

改用 rememberSaveable {} 后:
- 第一次进入页面时打印
re-calculate - 再切 Tab 回来时不会重新计算
scale会从保存状态中恢复

六、离开屏幕不等于 Activity 重建
这里还有一个容易混淆的点:
Composable leaving the screen is not recreation.
可组合项离开屏幕,不等于 Activity 或进程重建。
在 Compose 中:
- 离开屏幕:Composable 退出组合,
onDispose执行 - 再进入屏幕:Composable 重新进入组合
- Activity 重建:触发 Android 保存 / 恢复流程
常见“离开屏幕”的场景包括:
- Navigation 跳转或 Tab 切换
- LazyColumn item 滑出视口
- 条件渲染导致某段 UI 暂时不显示
因此,状态应该放在哪里,不能只看“会不会重组”,还要看它是否需要跨越组合生命周期。
七、如何选择
可以按下面的规则判断:
| 场景 | 推荐 |
|---|---|
| 只需要在普通重组中保持状态 | remember {} |
| 页面离开后再进入仍要恢复状态 | rememberSaveable {} 或上层状态持有者 |
| Activity / 进程重建后要恢复状态 | rememberSaveable {},必要时自定义 Saver |
| 状态属于业务数据或跨多个页面共享 | ViewModel / Repository / 状态管理层 |
| LazyList 滚动位置 | rememberLazyListState 等可保存状态 API |
对本文的 scale 来说,它是 UI 状态,类型简单,而且需要在离开页面后恢复,所以 rememberSaveable 是合理选择。
八、总结
remember 和 rememberSaveable 的区别,可以用一句话概括:
remember记住当前组合里的值,rememberSaveable记住可以被保存和恢复的状态。
排查这类问题时,重点不要只看“有没有重组”,而要继续追问:
- 这个 Composable 是否会退出组合?
- 状态是否需要在再次进入页面时恢复?
- 这个状态能不能被保存到 Bundle?
- 它应该属于 UI 局部状态,还是应该上移到 ViewModel?
理解这几个问题后,就能更稳定地选择 remember、rememberSaveable 或更上层的状态持有方式,避免页面切换后状态突然丢失。