remember VS rememberSaveable

引言

在 Jetpack Compose 中,我们常需要「记住」某个计算结果,避免在重组时重复计算以提升性能。Compose 提供了两种常用方式:remember {}rememberSaveable {}。二者都能在重组时复用值,但持久化范围不同,选错会导致状态在特定场景下被重置。本文结合一次实际问题和文档/实验,说明两者的区别与使用场景。

一、问题现象

页面中有两个主要部分:Dashboard(根据滚动做缩放动画)和 Asset List(可滚动列表)。用 remember {} 保存 scale 状态时:

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

Recording

改为 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 被重置。
    Logger
  • **改用 rememberSaveable {}**:只有第一次进入时打印一次 re-calculate,之后切 Tab 再回来也不会再打印,说明一直用的是已保存的值。
    Logger1

四、延伸:离开屏幕 ≠ 重建

Jetpack Compose 相关讨论 里有一句:**”Composable leaving the screen is not recreation”**(可组合项离开屏幕并不等于重建)。

在 Compose 中:

  • 组件离开屏幕会触发 onDispose,生命周期结束。
  • 再次进入屏幕会重新组合,相当于新的生命周期,remember {} 会重新计算。

所以「离开屏幕」和「Configuration 变化导致 Activity 重建」是两回事:前者不会触发 SavedInstanceState 流程,只会让可组合项退出组合;后者才会触发进程/Activity 重建,此时才用到 rememberSaveable 的持久化。

常见「离开屏幕」场景包括:

  1. Navigation 跳转 / Tab 切换(如本文案例)
  2. 列表滑动:Item 滑出视口时会退出组合,onDispose 会执行

例如 LazyColumn 的 Item 配合 DisposableEffect 可以观察到滑出时打印;若需要跨页面或重新进入页面时恢复列表滚动位置,应使用基于 rememberSaveablerememberLazyListState 等 API。

五、如何选择

需求 推荐
状态只需在重组过程中保持 remember {}
状态需要在 Activity/进程重建(如旋转、内存回收)后恢复 rememberSaveable {}
状态需要跨页面或更复杂的作用域 ViewModel 等;若仅在单页内且需应对「离开再进入」,rememberSaveable {} 仍适用

理解「进入/重组/退出组合」与「SavedInstanceState 重建」的差异后,就能在开发中按场景选用 rememberrememberSaveable,避免状态被意外重置。