1. 为什么需要跨模块 ViewModel 状态隔离
在大型移动应用的模块化演进中,ViewModel 状态边界模糊是最隐蔽、破坏力最强的架构腐蚀因素之一。当多个模块共享同一个 ViewModel 实例或直接读写彼此的状态片段时,系统会不可逆地滑向高度耦合、不可预测、难以测试的状态泥潭。本章从四个核心痛点切入,界定状态边界的概念,并为后续的隔离方案建立问题域基线。
1.1 问题全景:四大痛点对比
下表从表现、影响、典型场景、恶化速度四个维度,对四类核心问题进行横向对比,帮助团队快速定位自身项目所处的阶段。
| 痛点 | 表现 | 影响 | 典型场景 | 恶化速度 |
|---|---|---|---|---|
| 单体 ViewModel 膨胀 | 单个 VM 管理 ≥3 个不相关模块的状态,代码量 >500 行 | 可读性骤降、维护成本线性增长 | 页面级 VM 同时持有业务逻辑、UI 状态、缓存数据 | 中 |
| 模块间状态泄露 | 模块 A 直接读写模块 B 的 LiveData / StateFlow | 状态变更不可追踪,回滚困难 | 跨模块 EventBus / 全局状态钩子滥用 | 快 |
| 协作冲突 | 多人同时编辑同一 VM 文件,频繁合并冲突 | 开发效率下降 30%~50%,逻辑被意外覆盖 | 多团队共用一个 Feature VM | 极快 |
| 测试困难 | 单元测试需要 Mock 5+ 外部依赖,单测耗时 >2s | 测试覆盖率 <20%,CI 反馈周期拉长 | VM 构造器依赖多个模块的 Repository | 慢但致命 |
1.2 痛点深度分析
① 单体 ViewModel 膨胀 — 状态无边界
当 ViewModel 的职责从"管理单个页面的状态"异化为"协调多个模块的数据"时,膨胀就开始了。典型特征包括:单一 VM 文件超过 500 行、持有的 LiveData/StateFlow 数量超过 10 个、包含来自 3 个以上不同模块的数据转换逻辑。膨胀的直接后果是任何一次状态变更都需要在数百行代码中定位,代码评审形同虚设。
② 模块间状态泄露 — 信任链断裂
状态泄露是指一个模块未经授权地修改了另一个模块的内部状态。在代码层面表现为:模块 A 通过全局事件、共享引荐或反射直接写入模块 B 的 ViewModel 属性。这种泄露破坏了模块自治的信任基础——状态的所有权不再清晰,bug 定位时无法确定"谁在什么时候修改了它"。
③ 协作冲突 — 多人踩踏同一片状态
在 3~8 人的 feature 团队中,如果多个成员同时操作同一个 ViewModel,平均每周发生 2~3 次合并冲突,且冲突往往集中在状态初始化、数据刷新、错误处理等关键路径上。更隐蔽的风险是逻辑覆盖——A 提交的分页逻辑被 B 的 UI 状态修改无意中覆盖,导致线上数据加载异常。
④ 测试困难 — 依赖地狱
一个膨胀的 ViewModel 构造器往往需要传入 5~8 个外部依赖(多个 Repository、UseCase、缓存管理器等)。编写单元测试时,Mock 这些依赖的配置代码比测试逻辑本身还长,导致开发者倾向于放弃单元测试,转而依赖手工验证。测试覆盖率跌破 20% 后,重构风险急剧升高,形成不敢改 → 越改越乱 → 更不敢改的恶性循环。
1.3 边界定义与分层策略
状态边界是指:一个模块所持有的 ViewModel 状态数据,在写入权限、生命周期、可见范围三个维度上形成的安全围栏。边界清晰的状态具备三个特征:
- 写入权封闭:只有本模块内的代码可以修改状态值;
- 生命周期自治:状态的创建和销毁完全由本模块控制;
- 可见范围最小化:其他模块只能通过定义的接口(而非直接引用)读取状态快照。
从分层策略看,跨模块状态隔离的第一道防线是接口层(Interface Layer)——每个模块对外暴露的应当是抽象的状态查询协议,而非具体的 LiveData 或 StateFlow 实例。第二道防线是数据流边界(Dataflow Boundary)——所有跨模块的数据传递必须经过单向通道,不允许双向绑定或共享可变引用。
| 分层 | 职责 | 边界控制手段 |
|---|---|---|
| 展示层(UI Layer) | 仅消费本模块 ViewModel 的状态 | 禁止 import 其他模块的 ViewModel 类 |
| 状态层(ViewModel Layer) | 管理本模块状态,对外提供只读查询 | 状态对象使用 sealed 或 internal 修饰 |
| 领域层(Domain Layer) | 跨模块数据转换与编排 | 通过 UseCase 协议解耦,不持有 ViewModel 引用 |
| 数据层(Data Layer) | 数据源统一管理 | 响应式数据流在模块边界处做 .shareIn / .stateIn 隔离 |
1.4 组件职责与信任链路
每个模块的 ViewModel 应当恪守单一状态域(Single State Domain)原则:一个 ViewModel 只对它所在的模块内部的状态变更负责。职责清晰后,组件间的信任链路才能建立:
- 模块 A 信任:模块 B 不会未经授权地修改 A 的状态;
- 模块 B 信任:A 对 B 的状态变更请求会通过明确定义的接口到达;
- 测试信任:每个 ViewModel 的依赖可以精确 Mock,测试结果可靠。
数据流与控制流在隔离体系中有严格区分:
- 数据流(Data Flow):状态从数据层 → 领域层 → ViewModel → UI,方向单一,不可逆;
- 控制流(Control Flow):跨模块的事件(如"用户登出""购物车变更")通过事件总线或消息队列传递,只传递信号,不传递状态;
- 信任链路(Trust Chain):每个跨模块访问都必须经过接口签名校验(编译期)和运行时边界断言(可选),确保没有"越狱"路径。
1.5 选型权衡与风险点
隔离不是免费的。在决定"是否需要跨模块状态隔离"之前,需要评估以下权衡:
| 维度 | 隔离方案 | 不隔离方案 |
|---|---|---|
| 开发速度(短期) | 初期降低 15%~25%,由于接口定义和边界守卫 | 初期快,但第 3 个月后断崖式下降 |
| 重构灵活度(长期) | 高,模块可独立演进 | 低,牵一发而动全身 |
| 测试成本 | 单模块测试成本低,集成测试成本可控 | 单测成本极高,集成测试几乎不可写 |
| 团队协作效率 | 边界清晰,并行开发冲突少 | 频繁冲突,需要大量同步会议 |