核心判定准则:如果一个可组合函数的状态需要被外部读取、修改或在重组中保持一致性,则该状态就必须被提升。被提升后的状态成为“单一数据源”,避免状态在多个层级中产生拷贝或不一致。
### 1.2 单向数据流架构
单向数据流是 State Hoisting 的实施基础。在 Compose 中,数据流形成一个闭环:
1. **状态向下传递**:父组件通过参数将当前状态值传递给子组件。
2. **事件向上传递**:子组件不直接修改状态,而是调用父组件提供的 lambda 回调,由父组件在回调中修改状态。
3. **重组驱动**:Compose 运行时监听状态变化,当状态被修改时,自动重组依赖该状态的所有组件。
这种架构消除了双向绑定的复杂性和难以追踪的隐式状态修改,使数据流的路径完全透明。
常见错误:在子组件内部使用 `var` 声明 `mutableStateOf` 并在回调中直接修改。这样做会切断父组件的控制,导致状态碎片化,测试时无法独立验证子组件的行为。
### 1.3 可测试性优势
状态提升直接带来了组件可测试性的质变。未提升状态的组件很难进行单元测试,因为状态变更被封装在组件内部,测试代码无法注入或断言内部状态。提升后的组件则变成**纯 UI 函数**:给定输入(状态 + 回调),它总是产生确定的输出(UI 布局 + 事件触发)。
以下是一个对比示例,展示传统方式与状态提升方式的差异:
// 未提升状态 — 不可直接测试
@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")
}
}
测试验证:对于
### 1.4 状态提升与测试夹具的关联
测试夹具(Test Fixture)是测试环境中用于提供一致、可预测状态的辅助代码。State Hoisting 让测试夹具的构建变得简洁:
- 只需要构造一个状态引用和一组模拟回调,即可驱动组件。
- 无需启动完整的应用上下文或依赖注入框架。
- 夹具可以单独维护和复用,与组件生产代码解耦。
下图展示了 State Hoisting 在单向数据流中的角色以及测试夹具如何接入该数据流:
### 1.5 概念差异对比
下表总结了传统状态管理方式与 State Hoisting 方式在多个维度上的差异:
| 维度 | 传统方式(状态内聚) | State Hoisting 方式 |
| --- | --- | --- |
| **状态位置** | 声明在使用状态的组件内部 | 提升到父组件或 ViewModel |
| **数据流方向** | 双向绑定或内部突变 | 单向数据流(仅向下传递) |
| **可测试性** | 差,需要反射或集成测试环境 | 优秀,可构造纯函数测试 |
| **组件复用** | 低,状态绑定特定上下文 | 高,组件完全解耦 |
| **状态一致性** | 容易出现多份拷贝 | 单一数据源 |
| **测试夹具** | 需要模拟整个组件上下文 | 只需要注入状态 + 回调 |
GoodCounter,你可以在测试中构造一个 count 值,传入不同的 onIncrement 模拟事件,然后断言 UI 是否正确渲染。完全不需要创建真实的 ViewModel 或业务逻辑。
章节小结:State Hoisting 将状态管理从组件内部抽离到父级,使子组件成为纯 UI 函数。这种模式天然适合测试夹具分离:父组件是状态持有者,子组件无需感知测试夹具的存在,测试夹具只负责提供预定义的状态值和捕获事件参数,从而实现组件级的快速验证。