第1章 单向数据流(UDF)核心原理
1.1 什么是单向数据流
单向数据流(Unidirectional Data Flow,UDF)是一种架构模式,核心特征是数据沿着一个方向流动:从数据源(通常是 ViewModel 或 Store)向下流向 UI,用户操作则以事件形式向上回传。在 Jetpack Compose 中,UI 是无状态的纯函数,状态由外部持有并通过 remember / collectAsState 传递。
| 特性 | 说明 | 示例 |
|---|---|---|
| 数据向下 | 状态从 ViewModel 流向 Composable | val state by viewModel.uiState.collectAsState() |
| 事件向上 | UI 通过回调函数或 lambda 传递用户动作 | onClick = { viewModel.dispatch(Event.Increment) } |
| 不变性 | 状态对象不可变,每次更新都创建新实例 | data class UiState(val count: Int = 0) |
| 可预测性 | 单一数据源 + 确定性的状态转换函数 | 相同的输入 → 相同的 UI 输出 |
💡 核心思维:把 UI 当作状态的“投影”,而不是状态的“管理者”。所有对状态的修改都必须经过严格的单向管道。
1.2 数据向下,事件向上:UDF 的流动模型
下图展示了 Compose 中 UDF 的典型流程。状态从 ViewModel 向下流入 Composable,用户点击等事件沿箭头向上回到 ViewModel,ViewModel 通过 Reducer 产生新状态,再次向下渲染。
1.3 不变性(Immutability)
在 Compose 中,状态对象必须保持不可变。这意味着你不能修改已有对象的属性,只能创建新对象来反映变化。不变性带来的好处:
- 安全的并发:多个 Composable 可以安全地读取同一状态。
- 易于调试:状态变化可以被记录为快照序列。
- 触发重组:Compose 通过检测引用变化(而非属性变化)来决定是否重组。
// 错误:可变对象
data class MutableUiState(var count: Int = 0)
// 正确:不可变对象
data class ImmutableUiState(val count: Int = 0)
// 更新时不能直接修改 var 属性,而应复制
fun increment(state: ImmutableUiState): ImmutableUiState =
state.copy(count = state.count + 1)
⚠️ 常见错误:在 Compose 中直接修改
MutableState 的 value 是允许的(例如 mutableStateOf(0)),但如果是复杂对象,必须确保对象内部属性也是不可变的,否则 Compose 无法感知变更。1.4 可预测性(Predictability)
UDF 的可预测性建立在两个基础上:
- 单一数据源:每个状态片段的“真实来源”只有一个(通常是 ViewModel 中的
StateFlow或MutableState)。 - 纯函数式更新:Reducer(或 ViewModel 中的方法)必须是纯函数——给定当前状态和事件,返回新状态,无副作用。
| 原则 | 违反示例 | 正确做法 |
|---|---|---|
| 无副作用 | 在 Reducer 中写数据库 | Reducers 只负责计算新状态 |
| 同步更新 | 在 Reducer 中启动协程 | 协程在 ViewModel 层启动,结果通过 Reducer 同步处理 |
| 确定性 | 依赖外部随机数 | 事件包含完整信息,状态转换不依赖环境 |
1.5 实现流程:从事件到 UI 重组的四步闭环
- UI 组件发送事件:例如
onClick = { viewModel.onEvent(Event.Add) } - ViewModel 处理事件:调用 Reducer 生成新状态
- 状态向下流动:
StateFlow/MutableState将新状态推送给 Composable - Compose 重组:仅更新发生变化的 UI 节点
// ViewModel 示例
class CounterViewModel : ViewModel() {
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()
fun onEvent(event: CounterEvent) {
_uiState.value = when (event) {
CounterEvent.Increment -> _uiState.value.copy(count = _uiState.value.count + 1)
CounterEvent.Reset -> UiState()
}
}
}
// Composable 使用
@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
val state by viewModel.uiState.collectAsState()
Column {
Text("Count: ${state.count}")
Button(onClick = { viewModel.onEvent(CounterEvent.Increment) }) {
Text("+1")
}
}
}
1.6 调试技巧:追踪状态流
- 日志插桩:在 ViewModel 的
onEvent中打印旧状态、事件和新状态。 - 使用
SnapshotStateObserver:Compose 的debug模式可以跟踪重组原因。 - 时间旅行调试:如果使用类似 Redux 模式,可以记录状态快照序列。
// 调试日志模板
fun onEvent(event: CounterEvent) {
val oldState = _uiState.value
val newState = reducer(oldState, event)
Log.d("UDF", "Event: $event, oldState: $oldState, newState: $newState")
_uiState.value = newState
}
🔍 调试时重点关注:事件是否被正确捕获?Reducer 是否返回了新对象?UI 是否观察到引用变化?
1.7 性能考量
- 避免不必要的重组:使用
derivedStateOf或remember进行跳过。 - 状态粒度:将状态拆成多个小型
StateFlow,而不是一个巨型状态对象。 - 不变性与 GC:创建不可变副本会产生小对象,但现代 JVM 的 GC 对短周期对象非常高效。
1.8 常见错误与解决
| 错误 | 现象 | 原因 | 修复 |
|---|---|---|---|
| 直接修改可变对象 | UI 不更新 | 引用不变,Compose 无法检测 | 使用 copy() 创建新对象 |
| 在 Composable 中发起网络请求 | 无限重组 | 请求产生副作用导致状态再变 | 将网络请求移到 ViewModel |
| 事件冒泡混乱 | 多个事件同时触发 | 未正确区分 UI 事件与业务事件 | 统一使用 sealed class 定义事件 |
| 忘记收集状态 | UI 显示空白 | 没有调用 collectAsState() | 添加 val state by ... |
掌握 UDF 核心原理后,第 2 章将深入 Compose 的 State 与 remember 机制,把“数据向下”的具体实现讲透。