状态 StateFlow、ViewModel 与 UI 收集

源码仓库ComposeDemo(分支 main

技术目标

掌握三件事:

  1. 单向数据流Event → ViewModel → UiState → UI
  2. StateFlowMutableStateFlow :热流、始终可读的当前快照;与「无限历史」的冷流心智不同。
  3. 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)。

首页与样例屏的分层(便于预览与单测):

  • HomeRouteviewModel() + collectAsStateWithLifecycle(),再把 state / onEvent 传给无 ViewModel 的 HomeScreen
  • StateSampleScreen:直接在 Composable 参数里 viewModel(),适合小型样例;若屏幕变复杂,可同样拆成 *Route + *Screen(state, onEvent)

3. 本仓库示例:StateSampleScreen

文件:

  • StateSampleContract.ktUiState 数据类、Event / Effect 密封类型,协议集中在一处。
  • StateSampleReducer.kt纯函数 reduceStateSample(state, event) → Pair<UiState, Effect?>,不依赖 Android,便于 JVM 单测。
  • StateSampleViewModel.kt:持有 _uiState / _effects,在 onEvent 里调 reducer 并 send effect。
  • StateSampleScreen.ktcollectAsStateWithLifecycle() + LaunchedEffect 收集 effectsSnackbar

3.1 为什么 Snackbar 走 Channel / Flow 而不是 UiState.showMessage: String?

技术上:Snackbar 是一次性 UI 事件 。若用 UiState.showMessage: String?,必须在显示后 清空 ,否则重组会再次读到非 null → 重复弹 ;清空时机若在错误线程或与动画竞态,还容易漏弹或双弹。用 Effect 通道把「消费发生在 UI 层一次 showSnackbar」写死,协议更清晰。

工程上也可用 SharedFlowreplay = 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 及以上 才向下游收集;低于该状态时暂停,回到前台后继续,且 StateFlowvalue 始终是最新,不会丢「最终状态」,只是中间帧可能不绘制。

需要更激进或更保守的策略时,可查 collectAsStateWithLifecycle(..., minActiveState = ...) 的 API 说明,按产品对「后台是否允许短暂收集」的要求调整。


4. viewModel() 作用域与导航

  • viewModel() 默认绑定当前组合树关联的 ViewModelStoreOwner (常为 NavBackStackEntryActivity)。
  • 同一 composable(route) { ... } 多次调用 viewModel() 且无自定义 key,通常得到同一实例;跨 route 则不同 store → 不同实例。
  • 常见坑:把本应「按屏独享」的 viewModel() 写在错误的父组合 里,或 NavHost 嵌套/参数变化导致 key 未区分 ,拿到意料之外的复用或重建。排障时先确认「这个 Screen 对应的 BackStackEntry 是谁」。

5. API 速查

API 用途
viewModel() 默认 ViewModelStoreOwner 为当前 LocalLifecycleOwner / 导航条目
collectAsStateWithLifecycle() 生命周期感知收集 StateFlow → Compose State
rememberUpdatedState 在长效副作用(如 LaunchedEffectDisposableEffect)里读「最新 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,可能不发射------要么不可变拷贝 + 新引用,要么确保比较语义符合预期。

往期推荐

《Compose 入门:@Composable、组合与重组》

《Android 高级工程师模拟面试问答》

相关推荐
匆忙拥挤repeat1 小时前
Android Compose 使用 CompositionLocal 将数据的作用域限定在局部
android
YF02111 小时前
Android 权限系统的演变与深度治理
android·app
左小左2 小时前
🔥🔥🔥 我用AI基于 Tauri + Vue 3 写了个 ADB 桌面工具,把命令行的脏活全干了
android·vue.js·rust
花花鱼2 小时前
android studio 图标的使用及处理
android·ide·android studio
JJay.2 小时前
什么时候该用 BLE,什么时候该用 SPP?很多 Android 项目一开始就做错了
android
山峰哥2 小时前
SQL性能飙升秘籍:从索引策略到查询优化全解析
android
黄林晴2 小时前
稳定性全面升级!Compose Multiplatform 1.11 RC 正式推送
android
恋猫de小郭2 小时前
实用性 Max ,新 Flutter & Dart Agent Skills 深度解读
android·前端·flutter
重生之小比特2 小时前
【MySQL 数据库】表的约束
android·数据库·mysql