Jetpack Compose 中 remember 和 rememberSaveable 的区别:一次 Tab 切换状态丢失排查

在 Jetpack Compose 中,remember {}rememberSaveable {} 都能“记住”状态。

但它们的保存范围并不一样。

这个差异在简单页面里不明显,一旦遇到 Tab 切换、页面离开后重新进入、Activity 重建等场景,就很容易出现状态被意外重置的问题。

这篇文章结合一次真实排查,说明:

remember 只保证当前组合生命周期内的状态复用;rememberSaveable 适合需要在离开页面后仍然恢复的可保存状态。


一、问题现象

页面里有两个主要部分:

  • Dashboard:根据滚动做缩放动画
  • Asset List:可滚动列表

最开始使用 remember {} 保存 scale 状态时,表现并不稳定:

  • 快速切换 Tab:看起来正常,scale 还能保持
  • 在其他 Tab 停留一段时间再切回:scale 被重置,Dashboard 突然放大

Tab 切换后 scale 状态变化

改成 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 可能会退出组合。

再次回到页面时,它不是简单“继续刚才的状态”,而是重新进入组合。

这正是 rememberrememberSaveable 差异出现的地方。


三、remember {} 保存的是什么

remember {} 的核心行为是:

在当前组合中记住计算结果,重组时复用这个值。

也就是说:

  • 初次进入组合:执行 block,计算并保存值
  • 普通重组:直接返回保存的值,不重新执行 block
  • 退出组合后再次进入:进入新的组合生命周期,block 会重新执行

所以在这次问题中,如果页面离开 Tab 后被移出组合,再回来时:

remember { mutableFloatStateOf(1.0f) }

会重新执行,scale 自然回到 1.0f

快速切换时之所以“看起来正常”,只是因为组合可能还没有被回收,并不代表 remember 能跨页面稳定保存状态。


四、rememberSaveable {} 多做了什么

rememberSaveable {}remember {} 类似,也会在组合中保存状态。

不同的是,它会把可保存的值接入 Compose 的保存机制,基于 SaveableStateRegistry / Android SavedInstanceState 进行恢复。

它的要点是:

  • 默认使用 autoSaver() 保存 Bundle 支持的类型
  • 支持 IntStringFloat 等基础类型
  • 自定义类型需要提供 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.

在本文这个场景里,scaleFloat,属于可保存类型,因此适合用 rememberSaveable 保存。


五、用日志验证

可以用下面这段代码验证 remember 在退出组合后会重新计算:

var scale by remember {
    println("=== re-calculate")
    mutableFloatStateOf(1.0f)
}

DisposableEffect(key1 = null) {
    onDispose { println("=== onDispose") }
}

DisposableEffectonDispose 会在退出组合时执行。

使用 remember {} 时:

  • 第一次进入页面:打印 re-calculate
  • 切到其他 Tab 停留后:打印 onDispose
  • 再切回来:再次打印 re-calculate,状态被重置

remember 退出组合后重新计算

改用 rememberSaveable {} 后:

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

rememberSaveable 恢复已保存状态


六、离开屏幕不等于 Activity 重建

这里还有一个容易混淆的点:

Composable leaving the screen is not recreation.

可组合项离开屏幕,不等于 Activity 或进程重建。

在 Compose 中:

  • 离开屏幕:Composable 退出组合,onDispose 执行
  • 再进入屏幕:Composable 重新进入组合
  • Activity 重建:触发 Android 保存 / 恢复流程

常见“离开屏幕”的场景包括:

  1. Navigation 跳转或 Tab 切换
  2. LazyColumn item 滑出视口
  3. 条件渲染导致某段 UI 暂时不显示

因此,状态应该放在哪里,不能只看“会不会重组”,还要看它是否需要跨越组合生命周期。


七、如何选择

可以按下面的规则判断:

场景 推荐
只需要在普通重组中保持状态 remember {}
页面离开后再进入仍要恢复状态 rememberSaveable {} 或上层状态持有者
Activity / 进程重建后要恢复状态 rememberSaveable {},必要时自定义 Saver
状态属于业务数据或跨多个页面共享 ViewModel / Repository / 状态管理层
LazyList 滚动位置 rememberLazyListState 等可保存状态 API

对本文的 scale 来说,它是 UI 状态,类型简单,而且需要在离开页面后恢复,所以 rememberSaveable 是合理选择。


八、总结

rememberrememberSaveable 的区别,可以用一句话概括:

remember 记住当前组合里的值,rememberSaveable 记住可以被保存和恢复的状态。

排查这类问题时,重点不要只看“有没有重组”,而要继续追问:

  • 这个 Composable 是否会退出组合?
  • 状态是否需要在再次进入页面时恢复?
  • 这个状态能不能被保存到 Bundle?
  • 它应该属于 UI 局部状态,还是应该上移到 ViewModel?

理解这几个问题后,就能更稳定地选择 rememberrememberSaveable 或更上层的状态持有方式,避免页面切换后状态突然丢失。