2. 无状态组件与有状态组件:职责划分、Compose函数签名、状态参数传递

2.1 核心概念与职责划分

在 Jetpack Compose 中,组件(Composable)按照状态管理方式分为两类:无状态组件(Stateless Composable) 和 有状态组件(Stateful Composable)。职责划分的核心原则是状态提升(State Hoisting)——将状态移至调用方,使子组件保持纯展示与事件回调,从而提升可复用性与可测试性。

特性无状态组件有状态组件
状态持有不持有任何可变状态通过 remember / mutableStateOf 持有内部状态
数据来源全部通过参数传入部分或全部状态自行管理
事件处理通过回调函数参数通知父组件内部直接修改状态
可复用性高(纯展示,不依赖外部环境)受限于内部状态逻辑
可测试性高(仅需 mock 输入参数与回调)需额外处理内部状态生命周期
典型场景按钮、文本、列表项、自定义组件表单控件、计数器、动画控制器

2.2 Compose 函数签名设计

无状态函数签名的特征:所有需要展示的数据作为参数,所有用户交互通过回调参数传递。有状态函数签名则包含内部状态声明(如 remember 块)且无对应外部参数。

无状态组件函数签名模式
@Composable
fun CounterDisplay(
    count: Int,             // 只读数据
    onIncrement: () -> Unit, // 事件回调
    onDecrement: () -> Unit
) {
    // 只负责展示,不保存任何状态
}
有状态组件函数签名模式
@Composable
fun CounterWithState(
    initialCount: Int = 0
) {
    var count by remember { mutableStateOf(initialCount) }
    // 内部可读写状态,事件直接修改 count
}
设计原则:优先编写无状态组件,仅在状态必须内部管理(如动画、定时器)时才使用有状态组件。对外暴露的公共 API 应尽量采用无状态签名。

2.3 状态参数传递机制

状态从父组件传递到子组件,子组件通过回调将变化传回父组件,形成单向数据流。典型实现分三步:

  1. 父组件使用 remember + mutableStateOf 或 ViewModel 持有状态;
  2. 父组件将状态作为参数传给无状态子组件;
  3. 子组件触发回调,父组件更新状态,触发重组。
常见错误:在无状态组件内部直接通过 var count by remember { mutableStateOf(...) } 改写参数,此举违反无状态约定,会导致状态来源混乱与重组 bug。

2.4 完整实现示例

以下代码展示一个计数器模块,包含有状态的 CounterScreen(状态所有者)和无状态的 CounterBody(纯展示组件)。

// --- 无状态组件:CounterBody.kt ---
@Composable
fun CounterBody(
    count: Int,
    onIncrement: () -> Unit,
    onDecrement: () -> Unit,
    onReset: () -> Unit
) {
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("当前值:$count", style = MaterialTheme.typography.headlineMedium)
        Spacer(modifier = Modifier.height(8.dp))
        Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
            Button(onClick = onDecrement) { Text("-") }
            Button(onClick = onIncrement) { Text("+") }
        }
        Spacer(modifier = Modifier.height(8.dp))
        TextButton(onClick = onReset) { Text("重置") }
    }
}

// --- 有状态组件:CounterScreen.kt ---
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    CounterBody(
        count = count,
        onIncrement = { count++ },
        onDecrement = { count-- },
        onReset = { count = 0 }
    )
}

2.5 调试与部署要点

  • 调试:在无状态组件中传入 count 参数时,可以在父组件打断点观察重组时机;使用 Compose Compiler 的 @Stable / @Immutable 注解优化跳过重组。
  • 部署:将无状态组件放在独立包(如 .components)中,与 ViewModel 分离,方便在多个 screen 之间复用。
  • 性能:无状态组件由于不持有 remember,重组开销仅限参数比较,比有状态组件更轻量。但要注意回调 lambda 的稳定性,使用 remember 包裹回调避免创建新对象。
最佳实践:使用 CompositionLocal 或 ViewModel 作为深层状态传递通道时,仍应保持子组件函数签名为纯参数形式,避免隐式依赖。

2.6 常见错误与规避

错误场景原因正确做法
子组件内直接 var count by remember { ... } 并修改参数混淆无状态/有状态界限通过回调让父组件更新状态
无状态组件中使用 LaunchedEffect 或 DisposableEffect副作用与状态提升冲突将副作用移至有状态所有者
回调 lambda 每次重组重新创建(不作为 remember 参数)导致子组件不必要的重组使用 remember { { ... } } 稳定引用
无状态 vs 有状态组件数据流对比 有状态组件 (Stateful) var count by remember 内部读写:count++ 事件直接修改状态 低可复用性 · 低可测试性 无状态组件 (Stateless) count: Int(参数传入) onIncrement: () -> Unit(回调) 仅展示 + 通知父组件 高可复用性 · 高可测试性 状态提升 回调通知 状态提升:数据下沉,事件上浮

2.7 稳定性与性能优化

无状态组件由于不依赖 remember 和 mutableStateOf,Compiler 可以更容易地推断其稳定性。但要注意:

  • 若参数为不可变类型(如 String、Int),默认跳过重组;若为可变集合,需使用 ImmutableList 或手动标记 @Stable。
  • 避免在无状态组件内创建匿名 lambda 作为默认参数值,如 onClick: () -> Unit = {},每次重组都会重新创建对象。应使用 remember { { } } 或顶级函数。
  • 对于高频更新的无状态组件(如滚动列表项),将回调 lambda 提升为 remember 闭包,减少重组开销。
优化示例:
// 错误:每次重组创建新 lambda
@Composable
fun BadItem(
    text: String,
    onClick: () -> Unit = { Log.d("TAG", "click") }
) { ... }

// 正确:稳定回调引用
@Composable
fun GoodItem(
    text: String,
    onClick: () -> Unit = remember { { Log.d("TAG", "click") } }
) { ... }

通过坚持无状态优先、状态提升的模式,整个模块的状态管理变得可预测、可审计,且便于后续迁移至 ViewModel 或 Flow 驱动的架构。