第1章 State Hoisting 核心概念:状态上提、重组触发条件、Compose 稳定类型
本章是《State Hoisting 避免重组合实战》的基石,围绕三个紧密关联的核心概念展开:状态上提(State Hoisting)、重组触发条件与Compose 稳定类型。只有透彻理解这三者的定义、原理与协作关系,才能在后续章节中精准定位过度重组的根因,并运用状态上提模式写出高性能、可维护的 Compose UI。
1.1 状态上提(State Hoisting)
定义:状态上提是一种将状态从子可组合项“提升”到调用方(父可组合项或 ViewModel)的设计模式。上提后的子组件变为无状态(Stateless),仅通过参数接收数据,通过回调触发事件,从而彻底解耦状态的读写职责。
核心原则:单向数据流——状态向下传递,事件向上传递。
状态上提的三大收益
- 可复用性:无状态组件可在不同父组件中直接复用,无需关心内部状态细节。
- 可测试性:状态逻辑集中在父组件或 ViewModel 中,可独立进行单元测试。
- 重组可控:状态变化引发的重组范围被限制在真正依赖该状态的组件树内。
实现流程(两步法)
- 将子组件内的
remember + mutableStateOf移除,改为val value: T参数与onEvent: (T) -> Unit回调。 - 在父组件中通过
remember持有状态,并将状态值传给子组件,同时传入更新回调。
代码示例:反模式 vs 正模式
// ====== 反模式:状态困在子组件中,无法复用 ======
@Composable
fun BadCounter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("点击了 $count 次")
}
}
// ====== 正模式:状态上提,子组件无状态 ======
@Composable
fun GoodCounter(
count: Int,
onIncrement: () -> Unit
) {
Button(onClick = onIncrement) {
Text("点击了 $count 次")
}
}
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
// 可以复用多个 GoodCounter,共享同一状态
GoodCounter(count = count, onIncrement = { count++ })
GoodCounter(count = count, onIncrement = { count++ })
}
1.2 重组触发条件
Compose 的重组(Recomposition)是指当可观察状态发生变化时,框架重新执行受影响的可组合项 lambda,以更新 UI 树。准确理解触发条件,是避免“过度重组”的第一步。
三大触发条件
| 触发条件 | 说明 | 示例 |
|---|---|---|
| State 值变化 | 通过 mutableStateOf / MutableStateFlow 等持有的状态发生写操作。 |
count++ 触发读取 count 的组件重组 |
| 传入参数变化 | 可组合项的 key 参数或普通参数的 equals 比较结果为 false。 |
Text(text = name) 中 name 变化 |
| Snapshot 失效 | Compose 的 Snapshot 系统标记了某个状态读取记录为“脏”。 | 跨协程修改状态时自动触发 |
重组范围规则:只有读取了变化状态的可组合项才会被标记为“需要重组”。未读取该状态的兄弟或父组件会跳过重组。
// 演示重组范围 —— 仅 Text 读取了 count,Button 不重组
@Composable
fun RecompositionScopeDemo(count: Int, onIncrement: () -> Unit) {
Text("当前计数: $count") // count 变化时重组
Button(onClick = onIncrement) {
Text("增加") // 不读取 count,不重组
}
}
调试技巧:在 Android Studio 中开启 “Show Recomposition”(Layout Inspector → 齿轮图标 → Show borders / Show recomposition),闪烁的边框即表示发生重组的组件。
1.3 Compose 稳定类型
重组触发条件中提到“参数变化通过 equals 比较”。但如果一个类型被 Compose 判定为不稳定(Unstable),则即使 equals 返回 true,Compose 仍会保守地触发重组。这就是“稳定类型”存在的意义。
分类与注解
| 注解 | 语义 | 编译期影响 | 典型场景 |
|---|---|---|---|
@Immutable |
所有属性均为不可变(val + 非可变类型) | Compose 认为该类型永远不变,跳过重组 | data class 全部使用 val + 基础类型 |
@Stable |
属性可变,但 Compose 可追踪其变化 | Compose 信任 equals 结果,仅在变化时重组 |
ViewModel 中的 StateFlow;含 MutableState 的类 |
| 未标注(默认) | Compose 无法推断稳定性 | 每次传入新实例都触发重组 | 普通 data class 含 var 字段 |
// ====== @Immutable 示例 ======
@Immutable
data class ImmutableUser(val name: String, val age: Int)
@Composable
fun UserProfile(user: ImmutableUser) {
Text("${user.name} - ${user.age}") // 仅当 user 实例变化时重组
}
// ====== @Stable 示例(含可变状态) ======
@Stable
class MutableUser(name: String, age: Int) {
var name by mutableStateOf(name)
var age by mutableStateOf(age)
}
@Composable
fun MutableUserProfile(user: MutableUser) {
Text("${user.name} - ${user.age}") // 属性变化时精准重组
}
// ====== 不稳定类型 —— 会触发过度重组 ======
data class UnstableUser(var name: String, var age: Int)
// 即使 name/age 未变,传入新实例也会导致重组
常见错误:在 data class 中使用 var 字段且未加 @Stable,会导致 Compose 将其标记为不稳定。每次父组件重组时,即使数据未变,子组件也会被迫重组。解决方案:使用 @Immutable + val,或 @Stable + mutableStateOf。
1.4 三者关系与实战对照
以下 SVG 架构图直观展示了状态上提、重组触发条件与稳定类型在 Compose 组件树中的定位与协作关系。