4. Composable 函数中的状态管理:remember、mutableStateOf 基础用法

概念解释

在 Jetpack Compose 中,Composable 函数本身是无状态的——它们可以通过重组多次执行。要存储并管理随时间变化的数据(状态),必须依赖两个基础工具:

  • remember:一个编译器级的“缓存钩子”,告诉 Compose 在首次组合时计算并保存某个值,在重组时跳过重新计算,直接复用之前的值。它本身不提供可观测性。
  • mutableStateOf:创建一个可被 Compose 观察的 MutableState<T> 对象。当该对象的 value 改变时,Compose 会自动追踪并触发依赖该状态的 Composable 重组。

两者通常配合使用:remember { mutableStateOf(初始值) } 创建一个既持久又可响应的状态。

要点:remember 负责“记忆”,mutableStateOf 负责“通知”。没有 remember,状态会在每次重组时重新创建;没有 mutableStateOf,Compose 无法感知变化,UI 不会刷新。

原理图解:重组触发流程

remember + mutableStateOf 重组触发流程 ① Composable 函数执行 ② remember { mutableStateOf(…) } 创建/复用 ③ 用户交互/事件改值 ④ 触发重组(执行 Composable 函数体) 每次重组 remember 不重新计算 mutableStateOf 值变化驱动重组

类比:教室里的“记录板”

  • remember 就像教室墙上的一块固定白板——每当老师(重组)走进教室,白板上的内容依然保留,而不会被擦掉重新写。
  • mutableStateOf 就像白板本身具有“传感器”——一旦有学生用笔改写了上面的数字,整间教室的电子屏(UI)就会立即同步更新。
  • 二者缺一不可:没有白板的记忆能力,每次老师进来都会重新画表格;没有传感器,改了数字大家也看不到变化。

常见误区

误区说明正确做法
❌ 只用 remember 存可变对象例如 remember { mutableListOf<Int>() },列表变化时 UI 不会更新必须用 mutableStateListOf 或 mutableStateOf(mutableListOf())。
❌ 在 remember 中传入 mutableStateOf 但不解包直接赋值给变量时可能丢失观察能力使用 by 委托或显式访问 .value。
❌ 在非 Composable 作用域用 remember只能在 @Composable 函数中使用改用 mutableStateOf 配合 ViewModel。
❌ 忘记 mutableStateOf 的不可变性mutableStateOf 中的值可以变,但状态对象本身不可变检测变异:state.value = newValue 才触发重组。
易错陷阱:remember { mutableStateOf(0) } 这样写,每次重组时 mutableStateOf 都会被重新创建吗?不会——因为 remember 的 lambda 只在首次组合时执行,后续重组直接返回缓存的对象。但注意 lambda 中不能有副作用(如网络请求)。

基础用法代码示例

下面是一个最典型的“计数器” Composable:

@Composable
fun Counter() {
    // 记住一个可观察的整数状态,初始值 0
    val count = remember { mutableStateOf(0) }

    Column(modifier = Modifier.padding(16.dp)) {
        Text("当前计数: ${count.value}")
        Button(onClick = { count.value++ }) {
            Text("增加")
        }
        Button(onClick = { count.value-- }) {
            Text("减少")
        }
    }
}

使用 by 委托可以简化写法,避免显式加 .value:

@Composable
fun CounterBy() {
    var count by remember { mutableStateOf(0) }

    Column(modifier = Modifier.padding(16.dp)) {
        Text("当前计数: $count")
        Button(onClick = { count++ }) {
            Text("增加")
        }
    }
}

应用边界

  • 适用场景:单个 Composable 内部的临时状态(如输入框内容、开关状态、展开/折叠)。
  • 不宜使用:需要跨 Composable 共享或持久化(如屏幕旋转后保留)的状态——应使用 rememberSaveable 或 ViewModel。
  • 性能注意:不要在 remember 的 lambda 中做繁重计算或 I/O,它只在首次执行,但若计算很慢仍会卡顿第一帧。此时可考虑 remember(keys) { … } 指定键来按需重新计算。
  • 顺序约束:remember 必须放在 Composable 函数体顶层(不能在条件分支或循环内调用),否则重组时计算键会错乱。

练习题(自测)

  1. 下面代码中点击按钮后,Text 显示会变化吗?为什么?
    @Composable
    fun Test() {
        val number = remember { 0 }
        Text("Number: $number")
        Button(onClick = { /* number 无法修改 */ }) { … }
    }
    不会。因为 remember { 0 } 返回普通 Int,不可变且不可被 Compose 观察。
  2. 如何修正上面的代码使其能响应点击增加?
    改用 var number by remember { mutableStateOf(0) },并在 onClick 中 number++。
  3. remember { mutableStateOf(…)} 和 mutableStateOf(…) 直接写在 Composable 中有什么区别?
    后者会在每次重组时重新创建状态对象,导致 UI 表现异常(之前的值丢失)。必须用 remember 包裹才能持久。