第3章 State、MutableState、remember 入门:声明式状态、重组触发、正确初始化
3.1 声明式状态模型原理
在 Jetpack Compose 中,UI 不再是手动命令式更新的“视图树”,而是由 状态(State) 驱动的声明式函数。当状态变化时,框架自动重组(Recompose)读取该状态的 Composable 函数,产生新的 UI 快照。这一机制的核心抽象是 State<T> 与 MutableState<T>,以及负责保持状态跨重组生存的 remember。
| 概念 | 类型 | 可变性 | 跨重组保持 | 典型创建方式 |
|---|---|---|---|---|
| State<T> | 接口 | 只读(get) | 依赖 remember | mutableStateOf() 向上转型 |
| MutableState<T> | 接口 | 读写(get/set) | 依赖 remember | mutableStateOf(初始值) |
| mutableStateOf() | 顶层函数 | — | — | 返回 MutableState 实例 |
| remember | 内联函数 | — | 是(按 key 缓存) | remember { mutableStateOf(...) } |
声明式状态的核心契约:状态是唯一真相源(Single Source of Truth),Composable 函数只依赖状态参数和 remember 保持的局部状态,框架对比前后状态快照,按需重组。
关键理解: 重组不是重新执行整个 Activity,而是仅重新执行那些读取了已变化状态的 Composable 函数或其 lambda 内部。这是 Compose 运行时通过“订阅-通知”机制实现的。
3.2 实现流程:状态驱动 UI 的完整链路
- 声明状态 — 在 Composable 内用
remember { mutableStateOf(初始值) }创建可保持的 MutableState。 - 读取状态 — 直接在 Composable 函数体中读取
state.value(或通过 Kotlin 委托by),建立隐式订阅。 - 修改状态 — 在事件回调(如 Button.onClick)中更新
state.value = 新值,触发快照对比。 - 调度重组 — Compose 运行时标记所有订阅了该状态的 Composable 为“脏”,放入重组队列。
- UI 更新 — 框架在下个帧信号到来时执行重组,重新调用对应的 Composable 函数,生成新 UI。
委托语法糖: 使用
var count by remember { mutableStateOf(0) } 可直接读写 count(等价于 getValue/setValue 委托),代码更简洁,但需注意 import import androidx.compose.runtime.getValue 和 import androidx.compose.runtime.setValue。
3.3 代码示例:从基础到正确初始化
示例1:基础计数器(正确写法)
import androidx.compose.material3.*
import androidx.compose.runtime.*
@Composable
fun CounterExample() {
// remember 让 MutableState 跨重组存活
var count by remember { mutableStateOf(0) }
Column {
Text("当前计数: $count")
Button(onClick = { count++ }) {
Text("增加")
}
}
}
示例2:错误初始化 — 忘记 remember
@Composable
fun BrokenCounter() {
// ❌ 每次重组都会创建新的 MutableState,count 永远为 0
var count = mutableStateOf(0)
Button(onClick = { count.value++ }) {
Text("计数: ${count.value}")
}
}
后果:每次点击触发重组后,mutableStateOf(0) 重新执行,count 被重置为 0,UI 永远显示 1(点击后立即回退)。
示例3:带 key 的 remember(正确初始化依赖)
@Composable
fun ProfileScreen(userId: String) {
// 当 userId 变化时,remember 重新计算,否则复用缓存
val userState = remember(userId) { mutableStateOf(fetchUser(userId)) }
Text("用户名: ${userState.value.name}")
}
关键规则: remember 的 key 列表为空时,计算仅在首次组合(Composition)时执行一次;key 变化时,重新执行计算并丢弃旧状态。适合作为“依赖项”的场景,避免状态错乱。
3.4 调试:观察重组与状态
- 重组日志: 在 Composable 函数体第一行添加
Log.d("Recompose", "当前函数名: $data"),对比每次重组时的日志输出。 - Layout Inspector: Android Studio → Tools → Layout Inspector 可实时查看当前 Composable 树各节点的状态值。
- Compose Compiler Metrics: 在 build.gradle 中启用
composeCompilerMetrics = true,生成重组稳定性报告,定位不必要的重组。
// 调试重组触发
@Composable
fun DebugCounter() {
var count by remember { mutableStateOf(0) }
Log.d("Recompose", "DebugCounter 重组, count=$count") // 每次重组输出
Button(onClick = { count++ }) {
Text("计数: $count")
}
}
3.5 常见错误与修复
| 错误模式 | 表现 | 原因 | 修复方案 |
|---|---|---|---|
| 忘记 remember | 状态每次重组重置 | mutableStateOf 在每次重组重新执行 | 包裹 remember { ... } |
| 在 remember 外创建可变状态 | 状态丢失或跨重组不一致 | 局部变量随重组销毁 | 移至 remember 内部 |
| 使用 var 直接存储对象 | UI 不更新 | 普通变量不触发重组 | 改用 mutableStateOf 包裹 |
| remember 缺少 key | 依赖变化时状态未更新 | 缓存未随输入刷新 | 添加正确的 key 列表 |
| 在非 Composable 作用域使用 mutableStateOf | 运行时报错 | 缺少 CompositionLocal 上下文 | 仅在 Composable 或 lambda 内使用 |
3.6 性能与稳定性优化
- 细粒度状态: 将大状态拆分为多个 MutableState,避免修改一个字段导致全部消费方重组。例如用
data class包裹多个字段时,考虑使用mutableStateListOf或derivedStateOf。 - remember key 的精确性: key 应只包含实际影响计算结果的输入,避免传入不稳定的 lambda 或大对象。
- 避免在重组中做耗时操作: 状态读取和修改应轻量;复杂计算用
derivedStateOf或LaunchedEffect移出重组路径。 - 稳定性标注: 对于非 Compose 原生类型(如 List、Map),使用
@Stable或@Immutable注解帮助 Compose 跳过不必要的对比。
// 优化示例:使用 derivedStateOf 避免重复计算
@Composable
fun ShoppingCart(items: List<Item>) {
// totalPrice 仅当 items 内容变化时才重新计算
val totalPrice by remember { derivedStateOf { items.sumOf { it.price } } }
Text("总价: $totalPrice")
}
稳定性核心原则: 状态尽量靠近使用它的 Composable,避免状态提升过高导致大范围非必要重组。恰到好处的状态作用域 = 更好的性能 + 更可维护的代码。
3.7 知识点小结
State是只读接口,MutableState继承自State并增加setter。mutableStateOf()创建可观察状态容器,remember保证它在重组中存活。- 声明式状态模型:读取 → 订阅;修改 → 通知 → 重组 → UI 更新。
- 正确初始化 =
remember { mutableStateOf(初始值) },依赖变化时使用 key 列表。 - 调试重组用日志 + Layout Inspector;优化重心在状态粒度和 remember 作用域。