第2章:MVVM架构分层与组件定义
2.1 边界划分原则
MVVM 在播放器状态管理中的核心价值是职责分离:View 只做界面渲染与用户事件采集,ViewModel 持有状态并协调业务逻辑,Model 管理播放器原生能力与持久化数据。三层之间通过单向依赖约束边界——View 依赖 ViewModel,ViewModel 依赖 Model,Model 不反向依赖任何上层。这一强制方向防止了状态管理中的循环引用和职责扩散。
2.2 三层边界定义与职责清单
| 层次 | 边界范围 | 核心职责 | 禁止行为 |
|---|---|---|---|
| View | UI 组件树、布局、动画、触摸事件 | 数据绑定展示、用户交互转发、生命周期感知 | 直接操作播放器、修改状态、发起网络请求 |
| ViewModel | 状态容器、业务编排、事件转换 | 管理播放状态(播放/暂停/进度/错误)、格式化数据、调度 Model 接口 | 持有 Context、直接引用 View 实例、执行 IO 操作 |
| Model | 播放器核心、网络/存储、系统资源 | 提供播放器控制 API、持久化播放记录、触发底层状态回调 | 持有 ViewModel 引用、涉及 UI 线程、直接发送 UI 事件 |
2.3 依赖关系与信任链路
依赖方向决定了数据流通路:
- 构造期:ViewModel 通过构造函数获得 Model 接口(例如
IPlayerModel),View 通过依赖注入或工厂获得 ViewModel 实例。 - 运行期:View 通过
LiveData/StateFlow订阅 ViewModel 的可观察状态;ViewModel 通过suspend函数或回调调用 Model 方法。 - 信任链路:Model 仅产生不可变状态快照,ViewModel 对状态进行校验与缓存,View 仅作纯渲染。任何对状态的非预期修改都必须追溯到 ViewModel 的 mutation 入口。
2.4 播放器场景下的选型权衡
为何 MVVM 优于 MVC 或 MVP?
- MVC 中 Controller 与 View 耦合紧密,播放器暂停/继续等高频状态更新易导致 View 层臃肿。
- MVP 中 Presenter 持有 View 接口,单元测试需 mock View,且 Presenter 生命周期难以独立于 Activity。
- MVVM 通过数据绑定解耦 View 与 ViewModel,ViewModel 可完全脱离 Android 框架测试;StateFlow/Flow 天然支持响应式状态,适合播放器连续进度、缓冲百分比的实时推送。
2.5 风险点与落地步骤
风险点一:状态泄露 —— ViewModel 在横竖屏切换或后台运行时仍然存活,其内部的播放器状态(如当前播放位置)若未正确恢复,可能导致 UI 显示错误。解决方案:使用 SavedStateHandle 持久化关键状态。
风险点二:边界模糊 —— 开发者在 View 层直接调用 Model 的播放控制方法,或在 Model 中处理 UI 动画。必须通过 Code Review 和架构检测(如依赖反转规则)强制归位。
落地步骤建议:
- 定义 Model 接口:包括播放控制、状态回调、持久化方法。
- 实现 ViewModel:编写状态容器(
sealed class PlayerUiState)及对应 mutation 方法。 - 将 View 层拆分为 Composable / Fragment,通过数据绑定订阅 StateFlow。
- 编写单元测试:验证 ViewModel 对 Model 调用的正确性和状态转换逻辑。
2.6 验收标准
- View 层中不存在任何
player.play()、model.loadUrl()等直接调用。 - ViewModel 中不存在
context、view相关引用,且所有播放器操作通过 Model 接口。 - Model 层不引用 ViewModel 类或接口,也不导入任何 UI 包。
- 当 View 被销毁重建时,ViewModel 存活且状态不丢失(验证横竖屏切换前后的播放进度一致)。
2.7 分层依赖与数据流结构
下图展示了 MVVM 三层之间的边界、数据流方向及信任链路。View 仅通过绑定消费状态,ViewModel 负责将用户意图转发至 Model,Model 完成后回调状态更新。
实践提示:在 ViewModel 中,将每个播放器状态(如 isPlaying、positionMs、bufferedPercent)封装为独立的 StateFlow,避免将整个大对象推送导致的 View 层冗余刷新。Model 层的回调应当运行在指定的调度器(如 Dispatchers.IO),ViewModel 通过 viewModelScope 收集并转换到主线程。