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 状态参数传递机制
状态从父组件传递到子组件,子组件通过回调将变化传回父组件,形成单向数据流。典型实现分三步:
- 父组件使用
remember+mutableStateOf或ViewModel持有状态; - 父组件将状态作为参数传给无状态子组件;
- 子组件触发回调,父组件更新状态,触发重组。
常见错误:在无状态组件内部直接通过
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 { { ... } } 稳定引用 |
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 驱动的架构。