在 Android 开发中,MVVM 是非常常见的 UI 架构模式。它通过 ViewModel 承接 UI 状态和用户交互,让界面层变得更加清晰。
但在真实项目里,随着业务不断增加,很多团队都会遇到同一个问题:
ViewModel 越写越胖,业务判断、网络请求、数据保存、异常处理全都堆在里面。
一开始这样写可能很方便,但当业务规则越来越复杂时,ViewModel 就会逐渐变成一个"业务大杂烩":难维护、难复用、难扩展。
Clean Architecture 的价值,正是在于帮助我们把这些职责拆开,让 ViewModel 回到它真正应该负责的事情上:管理 UI 状态。
本文通过一个"上传文件"的例子,说明 Clean Architecture 如何让 ViewModel 保持轻量,同时让业务逻辑和数据流职责更加清晰。
1. 问题示例:传统 MVVM 中的胖 ViewModel
假设有一个上传文件的需求,流程如下:

很多项目会直接把这些逻辑写在 ViewModel 中:

这段代码的问题在于,ViewModel 同时承担了太多职责:
- 判断用户是否登录
- 判断文件大小是否合法
- 调用上传接口
- 写入上传记录
- 捕获异常
- 转换 UI 状态
这些逻辑混在一起后,ViewModel 会越来越难维护。
今天只是检查登录和文件大小,明天可能还要加:
- 普通用户和 VIP 用户上传大小不同
- 某些文件类型禁止上传
- 每日上传次数限制
- 上传前需要风控校验
- 上传成功后需要同步多个业务状态
如果这些规则都继续堆在 ViewModel 中,ViewModel 很快就会失控。
2. 先区分三类职责
这个上传流程看起来是一整段逻辑,但其实可以拆成三类职责:
| 职责 | 示例 | 所属层 |
|---|---|---|
| UI 逻辑 | 显示成功、失败、登录提示 | Presentation / ViewModel |
| 业务逻辑 | 未登录不能上传、文件大小限制、VIP 规则 | Domain / UseCase |
| 数据流逻辑 | 调接口、读写数据库、缓存上传记录 | Data / Repository |
问题的根源通常不是 ViewModel 写了几行代码,而是:
不同层的职责混在了同一个类里。
Clean Architecture 要解决的,正是这种职责混乱的问题。
3. Clean Architecture 如何拆分上传逻辑?
Clean Architecture 的核心思想是:
让不同层负责不同职责,并且依赖方向保持单向。
一个常见的 Android 分层结构如下:

在这个结构中:
- ViewModel 不直接处理业务规则
- UseCase 负责业务流程和业务判断
- Repository 负责数据获取、数据提交和数据持久化
4. ViewModel:只负责 UI 状态转换
重构后,ViewModel 不再关心"上传的业务规则是什么",它只关心:
UseCase 返回了什么结果,我应该展示什么 UI 状态。
一种常见写法是让 UseCase 返回一次性的业务结果:

这样一来,ViewModel 的职责就非常清晰:
- 接收 UI 事件
- 调用 UseCase
- 将业务结果转换成 UI 状态
它不再关心文件大小限制是多少,也不关心上传记录怎么保存。
不过,如果上传流程不是"一次请求、一次结果",而是包含上传进度、任务状态、队列状态等持续变化,那么可以进一步使用 Flow + StateFlow。
5. 更进一步:UseCase 暴露 Flow,ViewModel 使用 stateIn
对于上传这类场景,很多时候 UI 需要观察的不只是最终结果,还包括:
- Idle
- Loading
- Progress
- Success
- Error
这种情况下,让 UseCase 暴露一条持续的状态流会更自然。

UseCase 可以负责维护上传过程中的业务状态:

然后 ViewModel 不再手动维护 _uiState,而是直接把 UseCase 的业务状态流转换成 UI 状态流:

这样 ViewModel 就只剩两件事:

这种写法对于 Compose 或基于 Flow 的页面非常友好:

UI 只需要订阅 uiState,状态变化会自动驱动界面更新。
6. UseCase:承载业务规则和业务流程
无论 UseCase 返回一次性的 UploadResult,还是暴露持续的 Flow<UploadResult>,它的核心职责都一样:
处理业务规则,编排业务流程,输出明确的业务结果。
例如:

UseCase 适合处理这些内容:
- 业务规则判断
- 多个 Repository 的协调
- 业务流程编排
- 将底层异常转换为领域结果
- 输出明确的业务结果或业务状态流
例如后续需求变成:

这些规则都可以继续放在 UploadUseCase 中,而不是塞回 ViewModel。
这就是 ViewModel 不会变胖的关键。
7. Repository:专注数据访问,不承载业务规则
Repository 的职责是数据流,而不是业务规则。

Repository 负责:
- 从接口获取数据
- 向接口提交数据
- 读写数据库
- 管理缓存
- 屏蔽具体数据来源
但它不应该负责:
- 判断用户是否允许上传
- 判断文件大小是否符合业务要求
- 判断失败后 UI 应该展示什么
一个简单判断标准是:
如果这条规则来自产品需求,它通常应该放在 UseCase。
如果这段逻辑是在处理数据从哪里来、到哪里去,它通常属于 Repository。
8. 为什么返回 Result,而不是直接抛异常?
在业务开发中,失败通常可以分为两类。
业务失败
例如:
- 用户未登录
- 文件过大
- 上传次数超过限制
- 当前用户没有权限
这些失败是业务流程的一部分,不一定是异常。
系统异常
例如:
- 网络错误
- 服务端异常
- 数据库写入失败
- JSON 解析失败
这些才更接近真正的异常。
因此,UseCase 返回一个明确的 UploadResult,可以让调用方清楚知道这次操作的业务结果。

或者在持续状态场景中:

这样做有几个好处:
- ViewModel 不需要理解底层异常
- UI 状态转换更直观
- 业务失败和系统异常边界更清晰
- 业务结果可以被明确建模
9. 什么时候用 suspend Result,什么时候用 Flow?
这两种写法都可以,关键看业务场景。
如果是简单的一次性操作:

使用 suspend fun upload(file): UploadResult 就足够了。
如果业务过程存在连续状态:

或者需要页面持续观察:

那么使用 Flow<UploadResult> 或 StateFlow<UploadResult> 会更加自然。
可以简单总结为:
| 场景 | 推荐方式 |
|---|---|
| 一次请求,一次结果 | suspend fun(): Result |
| 有进度、有队列、有持续状态 | Flow<Result> / StateFlow<Result> |
| UI 需要长期订阅状态 | ViewModel 使用 stateIn 转成 StateFlow |
10. 总结
Clean Architecture 能让 ViewModel 保持轻量,核心原因是它把职责拆开了:
| 角色 | 职责 |
|---|---|
| ViewModel | 接收 UI 事件,暴露 UI 状态 |
| UseCase | 处理业务规则,编排业务流程 |
| Repository | 负责数据访问,屏蔽数据来源 |
对于上传文件这个例子来说:
- ViewModel 不再判断能不能上传
- UseCase 决定上传规则和业务结果
- Repository 只负责真正的上传和记录保存
最终得到的代码结构会更加清晰:

如果业务是一次性的,UseCase 可以返回 suspend Result。
如果业务状态是持续变化的,UseCase 可以暴露 Flow<Result>,ViewModel 通过 map + stateIn 转成 UI 可观察的 StateFlow。
换句话说:
MVVM 主要解决 UI 层的组织问题。
Clean Architecture 解决的是业务逻辑和数据流的职责划分问题。
当业务越来越复杂时,这种分层能避免 ViewModel 不断膨胀,也能让业务逻辑更集中、更容易复用、更容易维护。