第4章 状态容器选型:ViewModel vs Compose State vs Flow

在 Compose 埋点状态流转体系中,选择合适的状态容器决定了数据的可信度、可追溯性与可维护性。本章从边界、分层、职责、数据流与控制流、信任链路五个维度,系统对比 ViewModel、Compose State 与 Flow 三类容器,给出选型权衡矩阵与风险应对策略。

状态容器选型决策与流转关系图 状态容器选型决策与流转关系 用户事件 / 埋点触发 状态容器选型决策网关 瞬时UI状态 业务/跨屏状态 异步数据管道 Compose State remember/mutableStateOf 局部UI状态,重组作用域 ViewModel viewModel() / ViewModelStore 跨配置变更,业务状态 Flow StateFlow / SharedFlow 异步数据流,事件管道 Compose UI 重组消费 埋点事件上报 选型原则:UI 局部状态 → Compose State | 跨屏业务状态 → ViewModel | 异步数据管道 → 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 个关键维度对比三类容器,协助架构决策:

维度ViewModelCompose StateFlow
生命周期作用域跨屏/跨配置单 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 落地步骤

  1. 步骤一:状态盘点 — 梳理埋点需求中的状态字段,区分“业务上下文状态”与“UI 临时状态”。
  2. 步骤二:容器分配 — 业务上下文状态分配到 ViewModel(StateFlow 暴露);UI 临时状态分配到 Compose State(remember);异步事件管道使用 Flow。
  3. 步骤三:信任链路绑定 — 在 ViewModel 中构建 StateFlow 作为单一可信源,Compose 层通过 collectAsStateWithLifecycle() 订阅。
  4. 步骤四:测试隔离 — ViewModel 单元测试验证业务状态;Flow 单独测试事件管道;Compose 测试只验证 UI 重组。
  5. 步骤五:验收检查 — 确认不存在 Compose State → ViewModel 的反向数据依赖,确认埋点事件 payload 全部来自 ViewModel 状态。

4.6 验收标准

验收项验收细则通过标准
状态分层合规业务状态不在 Compose State 中定义代码扫描 0 违规
信任链路可追溯任意埋点字段可追溯到 ViewModel 属性埋点事件链路图完整
生命周期安全collect 绑定 Lifecycle无 collectAsState 裸用
测试覆盖ViewModel + Flow 单测覆盖核心业务状态用例 >80%
性能基线重组次数无异常增量埋点场景重组次数与基线一致
架构自检清单:完成选型后,可围绕“边界是否清晰、分层是否单向、信任链路是否可追溯”三项核心原则进行代码审查。三类容器选型不是技术喜好问题,而是数据可信度与架构健壮性的权衡结果。