2. ViewModel 的边界原则:单一职责、最小暴露、生命周期绑定

2.1 原则总览:三把尺子界定 ViewModel 边界

ViewModel 的边界并非由框架强制划定,而是由三条相互制衡的设计原则共同定义。这三条原则分别回答了 ViewModel 的三个本质问题:做什么、露什么、活多久。任何跨模块边界泄漏的根源,都可以追溯到其中一条原则被打破。

原则核心追问边界作用违反后果
单一职责这个 ViewModel 应该负责什么?划定职责上限,防止逻辑膨胀职责扩散 → 跨模块耦合 → 难以测试
最小暴露外部模块必须看到什么?限定公开接口,隐藏实现细节内部状态泄漏 → 信任链断裂 → 紧耦合
生命周期绑定ViewModel 何时创建与销毁?确立存活范围,防止内存泄漏资源泄漏 → 状态残留 → 数据不一致
架构决策基点:三条原则按优先级排序为 生命周期绑定 > 单一职责 > 最小暴露。生命周期是硬约束,职责范围是设计选择,暴露程度是接口契约。任何 trade-off 都必须在不破坏生命周期绑定的前提下进行。

2.2 单一职责原则:聚焦 UI 状态与业务编排

ViewModel 的职责边界应当严格限定在 UI 状态的持有与转换、业务逻辑的编排与调度 两个维度。它不应当知道 View 的具体实现细节,也不应当直接操作数据源。

应当承担的职责

  • 状态持有:以不可变快照形式暴露 UI 所需的状态集合。
  • 状态转换:接收用户事件或系统信号,驱动状态变更。
  • 业务编排:协调 UseCase / Repository 的调用顺序与策略。
  • 跨模块桥接:作为当前模块对外暴露的唯一入口,封装内部逻辑。

不应承担的职责

  • 直接操作 View:不持有 View 引用,不调用 TextView.setText() 等 UI 方法。
  • 持有 Context / Activity 引用:造成内存泄漏与测试困难。
  • 直接执行网络请求或数据库操作:委托给 Repository 或 UseCase 层。
  • 处理纯 UI 逻辑:如动画控制、焦点管理、滚动位置等由 View 层自行负责。
职责边界检查清单:为每个 ViewModel 方法问三个问题——(1) 这个方法是否涉及 UI 状态?(2) 是否只做编排不做实现?(3) 是否与当前模块直接相关?任一答案为"否",则职责越界。

2.3 最小暴露原则:对外接口的克制设计

跨模块边界的本质是 信任链路。ViewModel 向外部模块暴露的接口越宽,信任链路越脆弱。最小暴露原则要求:只暴露不可变的 UI 状态和语义化事件,隐藏所有可变实现与内部状态。

公开 vs 隐藏 的边界划分

公开(对外模块可见)隐藏(模块内部私有)
不可变的 UI 状态(StateFlow / LiveData)可变的状态副本(MutableStateFlow)
语义化的用户事件(sealed class / 接口)内部回调、监听器注册
明确的 ViewModel 生命周期回调协程 Job、定时器、缓存池
模块边界 DTO(数据传输对象)内部实体、数据库模型

信任链路设计

跨模块调用方只能通过 ViewModel 公开的 不可变状态 和 事件通道 进行交互。调用方不持有 ViewModel 引用内部的可变字段,也不直接访问其协程作用域。信任链路的完整路径为:

UI 层 → 事件密封类 → ViewModel → 状态 StateFlow → UI 层

常见暴露陷阱:不要将 Repository、UseCase 或 DataSource 直接作为 ViewModel 的公开属性暴露给外部模块。这会导致外部模块绕过 ViewModel 直接操作数据层,破坏整个模块的封装边界。应通过 ViewModel 的方法或事件接口间接访问。

2.4 生命周期绑定原则:与界面同生共死

ViewModel 的存活范围必须与对应的界面生命周期强绑定。当界面销毁时,ViewModel 必须同步清理所有资源、取消协程、释放缓存,确保不产生悬空引用或状态残留。

生命周期绑定的三个层次

  • 创建绑定:ViewModel 在界面首次创建时初始化,由 ViewModelStoreOwner 管理。
  • 作用域绑定:通过 viewModelScope 启动的协程,在 ViewModel 清除时自动取消。
  • 销毁绑定:onCleared() 回调中释放非协程资源(如监听器、文件句柄、Socket)。

生命周期不绑定的典型风险

风险场景表现后果
协程泄漏ViewModel 已销毁,协程仍在运行内存泄漏、无意义网络请求、状态更新崩溃
监听器未注销跨模块回调仍持有 ViewModel 引用内存泄漏、野调用、数据污染
缓存未释放图片缓存、数据快照残留内存膨胀、界面闪退
定时任务未取消轮询、延时操作在销毁后继续执行电量消耗、无意义日志、异常崩溃
生命周期绑定黄金法则:ViewModel 中所有异步操作、资源句柄、监听注册的启动点与取消点必须对称。使用 viewModelScope 可覆盖 90% 的协程场景;对于非协程资源,在 onCleared() 中显式释放,并通过单元测试验证。

2.5 三原则协同:数据流与控制流

三条原则并非独立运作,而是在同一个 ViewModel 中形成 约束三角:单一职责定义了 ViewModel 的横向边界(做什么),最小暴露定义了纵向边界(露什么),生命周期绑定定义了时间边界(活多久)。三者共同决定了跨模块信任链路的完整形态。

ViewModel 边界三原则框架图 ViewModel 边界三原则框架图 数据流入 状态流出 约束:职责边界 约束:暴露边界 约束:生命周期边界 数据层 单一职责 做什么 最小暴露 露什么 生命周期绑定 活多久 ViewModel 状态 + 编排 UI 层 三原则协同机制 单一职责划定模块范围 → 最小暴露限定接口契约 → 生命周期绑定确保资源安全 三者共同构成跨模块信任链路的基础:数据从右侧流入,状态向左侧流出,边界由三原则守护

图中展示了三条原则如何从三个维度约束 ViewModel:单一职责 从横向界定职责范围,最小暴露 从纵向限定接口宽度,生命周期绑定 从时间维度确立存活区间。数据层经由 ViewModel 编排后,以不可变状态流向 UI 层,整个过程在三原则的约束框架内完成。

2.6 选型权衡与风险点

原则冲突时的优先级决策

  • 生命周期绑定 优先于 单一职责:当职责划分导致 ViewModel 存活时间超出界面生命周期时,必须拆分 ViewModel 而非延长其存活。
  • 单一职责 优先于 最小暴露:当职责清晰但接口较多时,应通过聚合接口(Facade 模式)而非合并职责来减少暴露。
  • 最小暴露 不优先于 生命周期绑定:即便暴露接口极少,若 ViewModel 生命周期管理不当,仍会导致内存泄漏。

常见反模式与风险

反模式表现破坏的原则改进方案
万能 ViewModel一个 ViewModel 管理多个不相关页面单一职责按页面/功能拆分为多个 ViewModel
全公开 ViewModel将 MutableStateFlow 直接暴露为 public最小暴露使用 StateFlow 公开,MutableStateFlow 私有
静态 ViewModel使用静态变量持有 ViewModel 实例生命周期绑定由 ViewModelStoreOwner 自然管理
泄漏的 ViewModel在 ViewModel 中持有 Activity 或 Fragment 引用生命周期绑定使用 EventBus / Channel 代替直接引用
胖接口 ViewModel对外暴露数十个方法或属性最小暴露按场景分组为多个小接口或使用状态聚合
高优风险:跨模块场景中,最小暴露原则被破坏 是最常见且最隐蔽的风险。开发者为图方便将内部状态直接暴露给其他模块,导致调用方绕过 ViewModel 直接修改数据,最终引发连锁状态不一致。任何跨模块访问都必须经过 ViewModel 的公开接口,不得直接操作其内部属性。

2.7 落地步骤与验收标准

落地实施步骤

  1. 职责梳理:为每个 ViewModel 列出其管理的所有 UI 状态和业务编排点,标记超出单一职责的部分并拆分。
  2. 接口审计:检查 ViewModel 的所有公开属性和方法,将可变状态改为不可变快照,将直接方法调用改为事件密封类。
  3. 生命周期对齐:确认 ViewModel 的创建与销毁对应正确的 ViewModelStoreOwner(Activity / Fragment / NavBackStackEntry)。
  4. 资源清理验证:在 onCleared() 中释放所有非协程资源,对 viewModelScope 启动的协程进行超时与取消测试。
  5. 跨模块边界契约:为跨模块调用的 ViewModel 编写接口契约文档,明确公开状态与事件的范围。
  6. 自动化检查:通过 lint 规则或单元测试约束 MutableStateFlow 的公开访问、禁止在 ViewModel 中持有 Context 引用。

验收标准清单

验收项验收方法通过标准
单一职责Review ViewModel 方法列表每个方法只做一件事,不混合 UI 操作与数据操作
最小暴露检查 public 修饰符无可变状态公开,无内部 Repository 直接暴露
生命周期绑定单元测试 + 内存监测界面销毁后无协程运行,无泄漏引用
跨模块信任链路接口契约检查外部模块只通过事件 + 不可变状态与 ViewModel 交互
资源释放onCleared() 覆盖测试所有注册的监听器、定时器、缓存均在 onCleared 中清理
验收底线:三条原则中任意一条未被满足,跨模块状态隔离即宣告失败。建议将验收标准纳入 CI 门禁,每次合并前自动触发原则合规检查,从工程流程上守住 ViewModel 的边界。