1. 状态提升概念与Compose核心原则:无状态组件、状态向下、事件向上

状态提升(State Hoisting)是 Jetpack Compose 中管理 UI 状态的核心模式,也是跨平台(KMP)实现一致数据流的基础。其本质在于将可变状态从可组合函数中移除,交由上层调用方持有,并通过单向数据流驱动 UI 更新。本章将拆解这一模式背后的三条不可违背的原则:无状态组件、状态向下、事件向上。

原则一:无状态组件(Stateless Composable)

一个组件如果内部不直接声明 var 状态,而是通过参数接收状态和事件回调,则称为无状态组件。它不具备修改自身状态的能力,所有状态变化都必须由外部驱动。

特性有状态组件无状态组件
状态声明内部使用 remember { mutableStateOf }通过参数接收状态
状态修改内部直接赋值通过回调交由外部处理
可测试性低(依赖内部状态)高(纯展示,可 mock)
复用性受限于内部状态逻辑任意调用方均可复用
跨平台兼容需适配平台差异纯 UI 逻辑,天然跨平台
实践要点:在 KMP 项目中,尽量将所有可组合函数设计为无状态组件,仅在顶层或 ViewModel 层面持有状态。这能显著降低平台相关代码的耦合。

原则二:状态向下(State Down)

状态数据始终由父级传递给子级,子级只能读取,不能直接修改。这意味着所有的状态来源必须位于组件树的上层,并通过函数参数向下传递。

KMP 中的典型用法:在 shared module 中定义 data class 作为不可变状态,通过 Room 或 Ktor 获取数据后传递到 UI 层。

原则三:事件向上(Event Up)

子组件通过回调函数(Lambda)将用户操作或内部事件传递给父级,由父级决定如何修改状态。父级更新状态后,新的状态会重新向下流动,形成闭环。

典型回调签名示例:

@Composable
fun CounterDisplay(
    count: Int,          // 状态向下
    onIncrement: () -> Unit,  // 事件向上
    onDecrement: () -> Unit
) {
    Row {
        Button(onClick = onDecrement) { Text("-") }
        Text("$count")
        Button(onClick = onIncrement) { Text("+") }
    }
}

状态提升的完整数据流

下面这张 SVG 图展示了无状态组件、状态向下、事件向上三者如何在 Compose UI 树中协同工作:

状态提升数据流(State Hoisting Flow) 状态持有者(ViewModel / 父组件) 状态向下 (State Down) 无状态组件(Stateless Composable) 只接收 props,不持有 local state 子组件 A 显示状态 子组件 B 触发操作 事件向上 (Event Up) 💡 数据流向:父级 → 子级(状态),子级 → 父级(事件),形成单向闭环。

四步实现流转过程

  1. 定义不可变状态模型:在 shared 层创建 data class UiState(val count: Int, ...)。
  2. 在 ViewModel 中声明可观察状态:使用 MutableStateFlow 或 mutableStateOf 封装。
  3. 向下传递:将 ViewModel 中的状态转为 Compose State 对象,通过参数传递给无状态组件。
  4. 向上触发:无状态组件调用回调函数(如 onClick),ViewModel 收到后通过 .value 或 .emit() 更新状态。

常见错误

错误 1:在无状态组件内部直接定义状态。 例如在组件内写 var count by remember { mutableStateOf(0) },导致该状态与 UI 树绑定,无法被外部统一管理。应通过参数接收。
错误 2:跳过事件向上,直接在子组件中修改传入的状态。 Compose 不允许直接修改通过参数传入的 State 对象(编译时可能警告或运行时崩溃)。必须通过回调由父级修改。
KMP 调试技巧:在 ViewModel 中使用 snapshotFlow 监听状态变化,并在日志中输出当前值。例如:
snapshotFlow { uiState.value }
    .onEach { state -> println("State updated: $state") }
    .launchIn(viewModelScope)
这能快速定位状态未正确下发的 Bug。

本章小结