第4章 状态容器选型:ViewModel vs Compose State vs Flow
在 Compose 埋点状态流转体系中,选择合适的状态容器决定了数据的可信度、可追溯性与可维护性。本章从边界、分层、职责、数据流与控制流、信任链路五个维度,系统对比 ViewModel、Compose State 与 Flow 三类容器,给出选型权衡矩阵与风险应对策略。
4.1 边界定义与组件职责
| 容器类型 | 边界 | 核心职责 | 生命周期 |
|---|---|---|---|
| ViewModel | 业务层 → 表现层边界 | 持有业务状态,跨配置变更存活,协调用例 | 与 Activity/Fragment 或 NavBackStackEntry 同生命周期 |
| Compose State | 表现层内部 | 驱动 UI 重组,局部临时状态 | 跟随 Composable 函数重组周期 |
| Flow | 数据层 → 表现层管道 | 异步数据传递,事件通知,背压处理 | 由 collect 生命周期管理(StateFlow/SharedFlow) |
边界判断口诀:“跨屏留存用 ViewModel,仅 UI 临时用 State,异步管道用 Flow”。埋点状态中,页面级曝光、点击上下文应放在 ViewModel;动画过程、局部交互高亮用 Compose State;实时事件流、服务端推送埋点用 Flow。
4.2 分层归属与数据流方向
三类容器在 Compose 埋点架构中分属不同层次,数据流方向与信任链路也各有差异:
- ViewModel → Compose State:ViewModel 通过
StateFlow暴露状态,Compose 层collectAsState()将其转为 Compose State,形成“业务状态 → UI 状态”的单向信任链。 - Compose State → ViewModel:用户交互事件通过回调触发 ViewModel 方法,写入业务状态,形成“UI 事件 → 业务处理”的反向控制流。
- Flow 独立侧:Flow 在数据层产生,通过 ViewModel 中转或直接 collect 到 Compose,负责异步事件通知(如推送到达、后台埋点批量上报)。
4.3 选型权衡矩阵
以下矩阵从 8 个关键维度对比三类容器,协助架构决策:
| 维度 | ViewModel | Compose State | Flow |
|---|---|---|---|
| 生命周期作用域 | 跨屏/跨配置 | 单 Composable 重组 | collect 协程作用域 |
| 状态持久性 | 可配合 SavedStateHandle | 无持久性 | 无持久性 |
| 可测试性 | 高(可 mock) | 低(依赖组合上下文) | 高(纯异步流) |
| 背压处理 | 不直接处理 | 不直接处理 | 原生支持 conflate/buffer |
| 线程安全 | 需手动加锁或使用 Flow | 仅主线程写入 | 协程安全 |
| 状态共享 | 多 Fragment/Composable 共享 | 不可跨 Composable 直接共享 | 多收集者共享(SharedFlow) |
| 埋点信任链路 | 业务状态权威来源 | UI 瞬时状态辅助 | 事件管道,可追溯 |
| 性能开销 | 低(对象常驻) | 极低(重组粒度可控) | 中等(协程调度) |
选型决策铁律:埋点上下文中的业务语义状态(页面名、来源、用户标识、当前Tab)必须由 ViewModel 持有作为单一可信源;Compose State 仅用于 UI 临时状态(上拉加载动画、折叠展开);Flow 用于埋点事件管道(曝光事件流、点击事件流),不应承载业务状态。
4.4 风险点与避坑策略
- 风险 1:State 在 ViewModel 中泄露
错误地将mutableStateOf直接定义在 ViewModel 内部,导致 ViewModel 持有 Compose 运行时引用,测试时无法隔离。
解决:ViewModel 只暴露StateFlow,Compose 层通过collectAsState()转换,隔离运行时依赖。 - 风险 2:Flow 收集时机与重组不匹配
在LaunchedEffect或collectAsState中未正确处理生命周期,导致埋点流量重复或遗漏。
解决:使用collectAsStateWithLifecycle()绑定 Lifecycle 状态,避免重复 collect。 - 风险 3:SharedFlow 事件丢失
使用SharedFlow作为埋点事件管道且配置了replay=0,在配置变更时事件丢失。
解决:关键埋点事件使用replay=1或结合 ViewModel 的SavedStateHandle持久化事件 ID。 - 风险 4:信任链路断裂
Compose State 在多个 Composable 间传递,导致埋点上下文的来源不明确,数据可信度下降。
解决:强制规定“业务埋点上下文只从 ViewModel 获取”,Compose State 仅用于 UI 状态变化,不承载埋点语义。
⚠ 信任链路红线:任何埋点状态若不能回溯到 ViewModel 中的单一可信源,即视为不可信状态,禁止直接上报。Compose State 中的 UI 临时状态不得出现在埋点 payload 中作为业务字段。
4.5 落地步骤
- 步骤一:状态盘点 — 梳理埋点需求中的状态字段,区分“业务上下文状态”与“UI 临时状态”。
- 步骤二:容器分配 — 业务上下文状态分配到 ViewModel(
StateFlow暴露);UI 临时状态分配到 Compose State(remember);异步事件管道使用 Flow。 - 步骤三:信任链路绑定 — 在 ViewModel 中构建
StateFlow作为单一可信源,Compose 层通过collectAsStateWithLifecycle()订阅。 - 步骤四:测试隔离 — ViewModel 单元测试验证业务状态;Flow 单独测试事件管道;Compose 测试只验证 UI 重组。
- 步骤五:验收检查 — 确认不存在 Compose State → ViewModel 的反向数据依赖,确认埋点事件 payload 全部来自 ViewModel 状态。
4.6 验收标准
| 验收项 | 验收细则 | 通过标准 |
|---|---|---|
| 状态分层合规 | 业务状态不在 Compose State 中定义 | 代码扫描 0 违规 |
| 信任链路可追溯 | 任意埋点字段可追溯到 ViewModel 属性 | 埋点事件链路图完整 |
| 生命周期安全 | collect 绑定 Lifecycle | 无 collectAsState 裸用 |
| 测试覆盖 | ViewModel + Flow 单测覆盖 | 核心业务状态用例 >80% |
| 性能基线 | 重组次数无异常增量 | 埋点场景重组次数与基线一致 |
架构自检清单:完成选型后,可围绕“边界是否清晰、分层是否单向、信任链路是否可追溯”三项核心原则进行代码审查。三类容器选型不是技术喜好问题,而是数据可信度与架构健壮性的权衡结果。