第1章 单向数据流(UDF)核心原理

1.1 什么是单向数据流

单向数据流(Unidirectional Data Flow,UDF)是一种架构模式,核心特征是数据沿着一个方向流动:从数据源(通常是 ViewModel 或 Store)向下流向 UI,用户操作则以事件形式向上回传。在 Jetpack Compose 中,UI 是无状态的纯函数,状态由外部持有并通过 remember / collectAsState 传递。

特性说明示例
数据向下状态从 ViewModel 流向 Composableval 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 产生新状态,再次向下渲染。

UDF 单向数据流架构图 State(状态) ViewModel Composable UI Event(事件) Reducer 数据的唯一入口是 ViewModel,UI 只能消费状态,不能直接修改状态 用户操作产生 Event → Reducer 计算新 State → State 向下流动 → UI 重组

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 的可预测性建立在两个基础上:

  1. 单一数据源:每个状态片段的“真实来源”只有一个(通常是 ViewModel 中的 StateFlow 或 MutableState)。
  2. 纯函数式更新:Reducer(或 ViewModel 中的方法)必须是纯函数——给定当前状态和事件,返回新状态,无副作用。
原则违反示例正确做法
无副作用在 Reducer 中写数据库Reducers 只负责计算新状态
同步更新在 Reducer 中启动协程协程在 ViewModel 层启动,结果通过 Reducer 同步处理
确定性依赖外部随机数事件包含完整信息,状态转换不依赖环境

1.5 实现流程:从事件到 UI 重组的四步闭环

  1. UI 组件发送事件:例如 onClick = { viewModel.onEvent(Event.Add) }
  2. ViewModel 处理事件:调用 Reducer 生成新状态
  3. 状态向下流动:StateFlow / MutableState 将新状态推送给 Composable
  4. 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 机制,把“数据向下”的具体实现讲透。