第一章 理解状态提升本质

在 Jetpack Compose 中,状态提升(State Hoisting) 是构建可预测、可测试 UI 的核心设计模式。它要求可组合函数本身不持有可变状态,而是将状态上移到调用方,并通过单向数据流驱动 UI 更新。本章从原理出发,剖析为什么必须提升状态、单向数据流如何运作以及无副作用的可组合函数如何保证可复用性。

1.1 为什么要在 Compose 中提升状态

Compose 的渲染由状态驱动——当状态变化时,框架会重组(Recomposition)受影响的 Composable。若状态被“困”在某个可组合函数内部,将导致:

  • 不可预测:子组件内部修改状态,父组件无法感知,难以控制 UI 行为。
  • 难以测试:内部状态使单元测试变得复杂(需模拟内部状态)。
  • 复用受限:包含内部状态的可组合函数无法在其他场景直接复用。
  • 性能风险:内部状态闭包可能导致意外保持引用,引发内存泄漏或重组风暴。

提升状态的核心收益:

  • 调用方完全控制状态生命周期。
  • 子组件变成无状态“纯 UI”函数,仅通过参数和回调与外界通信。
  • 状态变更可被追踪、日志化、测试。

1.2 单向数据流

单向数据流(Unidirectional Data Flow)是 Compose 状态提升的架构基础,包含两条明确路径:

  1. 数据向下传递:父 Composable 通过参数将状态传递给子组件。
  2. 事件向上报告:子组件通过回调(lambda)将用户操作或内部事件通知父组件,由父组件负责修改状态。

这种模式避免了双向绑定带来的数据混乱,确保状态变更的源头唯一(State Owner)。

模式 数据流向 测试难度 复用性 性能控制
内部状态 双向/混乱 高 低 弱
提升状态 单向 低 高 强

1.3 可组合函数的无副作用

理想的 Composable 应当是纯函数:给定相同输入(参数 + 稳定引用),始终输出相同 UI 树,不产生可观察的副作用。副作用包括:

  • 修改外部变量或全局状态。
  • 启动网络请求、数据库写入、文件 I/O。
  • 修改输入参数(即使参数是可变对象)。

若 Composable 内部持有了可变状态(例如 var count by remember { mutableStateOf(0) }),它就不再是纯函数——每次调用可能产生不同 UI,且父组件无法控制该状态。提升状态后,Composable 仅依赖传入的参数,所有副作用被提升至调用方(通常是 ViewModel 或父 Composable),保证可组合函数本身可检验、可缓存。

1.4 状态提升的实现流程

在编码层面,提升状态遵循以下步骤(以计数器为例):

  1. 识别状态:哪个数据需要被渲染、被修改?例如 count: Int。
  2. 定义回调:子组件需要修改状态时,通过参数传入一个 lambda(如 onIncrease: () -> Unit)。
  3. 提升状态:将 count 的声明移到调用方,子组件只接收 count 和 onIncrease。
  4. 事件处理:在父 Composable 中创建状态(remember { mutableIntStateOf(0) }),并在 onIncrease 内更新。
// 不推荐:内部状态
@Composable
fun BadCounter() {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

// 推荐:状态提升
@Composable
fun GoodCounter(count: Int, onIncrease: () -> Unit) {
    Button(onClick = onIncrease) {
        Text("Count: $count")
    }
}

@Composable
fun CounterScreen() {
    var count by remember { mutableIntStateOf(0) }
    GoodCounter(count = count, onIncrease = { count++ })
}

1.5 常见错误与避坑

常见错误 1:在 Composable 内部修改传入的 MutableState 对象,而非通过回调。例如直接在子组件内 count.intValue++,破坏单向数据流。

常见错误 2:忘记对状态使用 remember 或 rememberSaveable,导致重组时状态丢失。

常见错误 3:将整个 ViewModel 传给子组件,而非仅传递所需状态和回调,导致子组件对 ViewModel 产生硬依赖、难以复用。

1.6 调试技巧

  • 使用 Compose Compiler Metrics 输出重组日志,观察状态变化时哪些 Composable 被重组。
  • 在 ViewModel 或父 Composable 中添加日志,追踪状态变更的时间点。
  • 使用 Layout Inspector 查看 Composable 树和参数值,验证数据是否正确向下传递。

1.7 性能与稳定性优化

  • 提升状态可让 derivedStateOf、snapshotFlow 等优化工具在父级集中使用,避免子组件内部重复计算。
  • 避免将大型不可变数据(如列表)直接作为状态传递给多个子节点;考虑使用 key 或 Stable 注解提升重组跳过率。
  • 确保回调 lambda 是稳定的(使用 remember 或 viewModelScope 中的 lambda),防止子组件因回调变化而意外重组。

1.8 状态提升核心流程图

状态提升核心交互流程 父 Composable 状态: count (MutableState) 子 Composable (无状态) 参数: count, onIncrease UI 渲染(Text / Button) 数据向下(count) 事件向上 (onIncrease) 状态持有者 无状态 UI 渲染层 实线:数据流 / 虚线:事件流