1. 状态提升核心原理:无状态组件、状态下沉、单一数据源

状态提升(State Hoisting)是 Compose 中最核心的数据流设计模式,它将可变的 UI 状态从可组合函数中抽离,交由上层可控的 holder 管理,从而实现 可预测、可测试、可复用 的界面架构。本章从三个基石概念出发,还原状态提升的完整设计逻辑。

1.1 无状态组件(Stateless Component)

无状态组件指不拥有内部 remember 或 mutableStateOf 的可组合函数,它仅通过参数接收状态,并通过回调向上层发送事件。这种组件天然可复用,且不产生副效应。

特性有状态组件无状态组件
状态源头组件内部外部传入
修改方式直接改变内部状态通过 lambda 回调
测试难度需要模拟内部状态纯函数式,易测试
复用场景绑定特定逻辑可任意组合

实践要点:正式项目中推荐所有可组合函数默认写成无状态,仅在顶层作用域(Screen / ViewModel)中保留唯一状态持有点。

1.2 状态下沉(State Hoisting)

状态下沉是指将子组件的局部状态“提升”到父组件或 ViewModel 中,父组件通过 值 + 回调 的方式传递给子组件。子组件不再拥有可变状态,而是通过回调通知父组件变更。这样数据流变为单向:State → Child → Event → Parent → State。

标准提升签名:

@Composable
fun Counter(
    count: Int,          // 当前值
    onIncrement: () -> Unit, // 增加事件
    onDecrement: () -> Unit  // 减少事件
) {
    Row {
        Button(onClick = onDecrement) { Text("-") }
        Text("$count")
        Button(onClick = onIncrement) { Text("+") }
    }
}

调用处持有状态:

@Composable
fun CounterScreen() {
    var count by remember { mutableIntStateOf(0) }
    Counter(
        count = count,
        onIncrement = { count++ },
        onDecrement = { count-- }
    )
}

调试技巧:在 Compose 布局检查工具中,若发现某个状态的 read 和 write 出现在同一组件中,应当考虑是否可以将 write 上移。

1.3 单一数据源(Single Source of Truth)

单一数据源原则要求:同一份数据在状态树中只存在一个可写副本。所有派生数据、计算值、界面状态都必须从这个源头衍生。在 Compose 中,单一数据源通常由 ViewModel 或 StateHolder 实现。

违反单一数据源的反例(存在两份 count):

// 不推荐:两个组件各自维护独立的状态
@Composable
fun BadCounter() {
    var count by remember { mutableIntStateOf(0) }
    CounterA(count) { count++ }
    CounterB(count) { count++ }
}

正确做法是将状态统一托管:

// 推荐:单一数据源由 ViewModel 提供
class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() { _count.value++ }
    fun decrement() { _count.value-- }
}

1.4 状态提升完整流程图

状态提升核心结构 单一数据源 ViewModel / StateHolder 状态下沉 接收 State + 回调 无状态组件 只读 State + 发射 Event State 向下传递 State 向下传递 Event 回调 Event 上浮 数据流方向:单一数据源 → 无状态组件/状态下沉,事件反向传回 单一数据源保证数据唯一可信,状态提升消除重复状态源

1.5 常见错误与避坑

  1. 忘记提升回调:子组件直接修改 mutableStateOf 的参数(参数实际上是值复制,修改无效)。必须通过回调让父组件更新状态。
  2. 在多个层级重复创建同一状态:同一份数据在树的不同分支各自创建副本,导致界面显示不一致。
  3. 过度提升:将完全属于子组件的临时 UI 状态(如输入框的焦点)也提升到 ViewModel,反而增加复杂度。应根据“是否需要跨组件共享”来判断。
  4. 忽略重组范围:状态提升后父组件会随状态变化重组,若父组件计算量大,应使用 derivedStateOf 或 remember 裁剪。

1.6 性能与稳定性优化

  • 推荐使用 mutableIntStateOf / mutableFloatStateOf 而非 mutableStateOf<Int>,减少装箱开销。
  • 使用 remember 缓存 lambda:防止回调在重组时重新创建导致子组件不必要的重绘。
  • 观察重组范围:可在可组合函数内打印日志,确认状态提升后仅必要的组件重组。
  • 考虑使用 snapshotFlow:将 Compose 状态转换为冷流,配合 ViewModel 进行异步操作时避免遗漏重组。

注意:状态提升的粒度不宜过细。如果每个 TextField 都单独对应一个回调,代码会膨胀。通常将一组相关状态封装为一个 data class,通过一个回调更新整个对象。

1.7 本章小结