源码仓库 :ComposeDemo(分支
main)
技术目标
掌握三件事:
- 单向数据流 :
Event → ViewModel → UiState → UI。 StateFlow与MutableStateFlow:热流、始终可读的当前快照;与「无限历史」的冷流心智不同。collectAsStateWithLifecycle():只在生命周期达到阈值后收集,避免后台浪费与 非法状态更新。
1. StateFlow 在 UI 里的角色
StateFlow<T> 表示 当前快照 + 未来变化:
value读当前值;无订阅者时仍可被业务层读写。collect/collectAsStateWithLifecycle订阅变化;Compose 侧用后者与Lifecycle对齐。
MutableStateFlow 常用 update { it.copy(...) } 做 原子 状态迁移,避免 read-modify-write 竞态(多协程同时改同一状态时尤其重要)。
与冷流 / 历史缓冲的区别 :StateFlow 不面向「回放整条时间线」,而面向「可被整页 UI 替换的状态 」。业务上若需要「每个中间值都要被消费」(例如审计日志),应另用 Flow 或带缓冲的通道,而不是塞进 StateFlow。
2. 单向数据流(本仓库两种写法)
Event
emit
collectAsStateWithLifecycle
Effect 通道
Composable
ViewModel
StateFlow UiState
- 状态 :
StateFlow<UiState>→ UI 只读、通过事件回写。 - 一次性事件 (Snackbar、导航、震动):本示例用
Channel+receiveAsFlow(),避免把「已消费事件」塞进UiState导致重复触发(见下文 3.1)。
首页与样例屏的分层(便于预览与单测):
HomeRoute:viewModel()+collectAsStateWithLifecycle(),再把state/onEvent传给无 ViewModel 的HomeScreen。StateSampleScreen:直接在 Composable 参数里viewModel(),适合小型样例;若屏幕变复杂,可同样拆成*Route+*Screen(state, onEvent)。
3. 本仓库示例:StateSampleScreen

文件:
StateSampleContract.kt:UiState数据类、Event/Effect密封类型,协议集中在一处。StateSampleReducer.kt:纯函数reduceStateSample(state, event) → Pair<UiState, Effect?>,不依赖 Android,便于 JVM 单测。StateSampleViewModel.kt:持有_uiState/_effects,在onEvent里调 reducer 并sendeffect。StateSampleScreen.kt:collectAsStateWithLifecycle()+LaunchedEffect收集effects→Snackbar。
3.1 为什么 Snackbar 走 Channel / Flow 而不是 UiState.showMessage: String??
技术上:Snackbar 是一次性 UI 事件 。若用 UiState.showMessage: String?,必须在显示后 清空 ,否则重组会再次读到非 null → 重复弹 ;清空时机若在错误线程或与动画竞态,还容易漏弹或双弹。用 Effect 通道把「消费发生在 UI 层一次 showSnackbar」写死,协议更清晰。
工程上也可用 SharedFlow(replay = 0,适当 extraBufferCapacity) 表达 effect;Channel 在本仓库里更直观地表达「事件被消费即消失」。
3.2 LaunchedEffect(Unit) { viewModel.effects.collect { ... } } 注意点
- 单收集点 :避免多个
LaunchedEffect同时collect同一 Flow;否则同一 effect 可能被处理多次。 - 结构化并发 :
collect是挂起函数,LaunchedEffect随组合退出而 取消 ,collect一并取消;不要在 Composable 里用「野协程」挂到全局 scope 去收 effect。 - 若 Snackbar 要读 最新的
onDismiss/action回调,在长效collect内配合rememberUpdatedState,避免闭包捕获旧 lambda。
3.3 collectAsStateWithLifecycle 在做什么?
默认在 Lifecycle.State.STARTED 及以上 才向下游收集;低于该状态时暂停,回到前台后继续,且 StateFlow 的 value 始终是最新,不会丢「最终状态」,只是中间帧可能不绘制。
需要更激进或更保守的策略时,可查 collectAsStateWithLifecycle(..., minActiveState = ...) 的 API 说明,按产品对「后台是否允许短暂收集」的要求调整。
4. viewModel() 作用域与导航
viewModel()默认绑定当前组合树关联的ViewModelStoreOwner(常为NavBackStackEntry或Activity)。- 同一
composable(route) { ... }内 多次调用viewModel()且无自定义key,通常得到同一实例;跨 route 则不同 store → 不同实例。 - 常见坑:把本应「按屏独享」的
viewModel()写在错误的父组合 里,或NavHost嵌套/参数变化导致 key 未区分 ,拿到意料之外的复用或重建。排障时先确认「这个 Screen 对应的BackStackEntry是谁」。
5. API 速查
| API | 用途 |
|---|---|
viewModel() |
默认 ViewModelStoreOwner 为当前 LocalLifecycleOwner / 导航条目 |
collectAsStateWithLifecycle() |
生命周期感知收集 StateFlow → Compose State |
rememberUpdatedState |
在长效副作用(如 LaunchedEffect、DisposableEffect)里读「最新 lambda」 |
derivedStateOf |
从多个 State 派生计算,仅当派生结果变化时才让依赖方重组 |
MutableStateFlow.update { } |
原子更新,推荐替代「读 value → 算新值 → 写 value」 |
6. 风险与反例
StateFlow初值未设或语义不完整 :首帧collect前 UI 可能闪空或展示占位;UiState尽量用「加载中 / 成功 / 失败」等完备子状态表达。- 在 Composable 里起
rememberCoroutineScope().launch发网络请求 :Activity 旋转或离开即取消,易与「应在 ViewModel 里持久」的意图冲突;长生命周期工作 优先viewModelScope+ 在UiState里反映进度。 - 把导航指令塞进
StateFlow:与 Snackbar 类似,易出现「返回栈已变仍收到旧导航」;一次性导航更宜 effect 通道或SharedFlow单次消费约定。 equals不变的data class字段 :StateFlow仅在value按 equals 判定变化 时通知;若内部列表用同一引用mutate,可能不发射------要么不可变拷贝 + 新引用,要么确保比较语义符合预期。