为什么 Clean Architecture 能让 ViewModel 保持轻量?

在 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 不断膨胀,也能让业务逻辑更集中、更容易复用、更容易维护。

相关推荐
AD钙奶-lalala1 小时前
kotlin反射
android·开发语言·kotlin
le1616161 小时前
Android Compose——尺寸修饰符的调用顺序构成的不同尺寸约束效果
android·compose·modifier
折翅鵬9 小时前
Android史诗级网络优化实践总结
android·网络
赏金术士11 小时前
Android 项目模块化与 Feature 组件实践
android·kotlin·模块化
summerkissyou198715 小时前
Android-UI-获取屏幕尺寸的方法
android·ui
用户860225046747215 小时前
Kotlin 函数式编程入门与实践指南
android
最爱睡觉睡觉睡觉17 小时前
CSS → Flutter 对照手册
android·前端
xingpanvip17 小时前
星盘接口开发文档:马盘次限盘接口指南
android·开发语言·python·php·lua
用户261904985615718 小时前
JUnit4 完整配置流程
android