🧭 协程中的三大反模式:真正的问题不在 ViewModel
在 Android 项目中,协程早已成为默认选择。 但很多协程代码的问题,并不体现在"能不能跑",而是体现在职责边界是否清晰。
下面这段代码,在很多项目里都能看到:

代码能工作,但从架构角度看,问题已经出现了。
反模式一:ViewModel 决定线程模型
kotlin
viewModelScope.launch(Dispatchers.IO)
这行代码的核心问题不是"IO 用得对不对", 而是 ViewModel 不应该做这个决定。
ViewModel 的职责应当是:
- 组织业务流程
- 暴露 UI State
- 描述「要做什么」
而不是:
- 决定运行在哪个线程
- 关心 IO、CPU 等执行细节
一旦 ViewModel 指定了 Dispatchers.IO,意味着:
- UI 层开始感知线程模型
- Repository 被迫适配 UI 的执行策略
- 线程语义从数据层泄漏到了展示层
这是典型的职责倒置。
反模式二:UI 层手动切回 Main,是结构已经失衡的信号

这一行代码,本身就暴露了问题:
如果需要在 ViewModel 里"手动切回 Main",说明协程的启动上下文已经不合理。
结果是形成了这样的执行路径:
css
Main → IO → Main
问题不在于多了一次线程切换, 而在于 ViewModel 正在承担"线程编排"的职责。
当 UI 层开始负责调度线程时,分层就已经开始模糊。
反模式三:异常处理逻辑泄漏到 UI 层

这段代码表面上是在"兜底", 但从职责角度看,它暴露了一个更深层的问题:
UI 层正在直接感知底层异常。
这意味着:
- ViewModel 需要知道 Repository 会抛异常
- UI State 与异常类型产生耦合
- 网络错误、业务错误、数据错误在 UI 层被混为一谈
异常,是实现细节; 而 UI 层只应该关心结果语义。
正确的分层原则
Repository 的职责应当明确收敛为三点:
- 确定线程模型(IO / CPU)
- 捕获并转换异常
- 返回稳定、可消费的结果
ViewModel 的职责只有一件事:
将 Repository 的结果映射为 UI State
正确写法:线程与异常统一收敛到 Repository
Repository 层
或在需要更明确异常语义时:

线程切换和异常转换,在这里完成。
ViewModel 层

可以注意到:
launch使用默认 Main- 没有
Dispatchers - 没有
withContext - 没有
try-catch - ViewModel 只消费"结果"
为什么这种结构是唯一可扩展的?
当需求发生变化时:
- 增加缓存
- 增加重试
- 增加 fallback
- 增加日志与链路追踪
- 替换数据来源
所有改动都只发生在 Repository 层。
UI 层与 ViewModel 的代码无需调整。
总结:真正的反模式是什么?
| 表象 | 本质问题 |
|---|---|
| ViewModel 切 IO | 线程职责泄漏 |
| UI 手动切 Main | 协程结构失衡 |
| UI try-catch | 异常模型未收敛 |
结论
如果你的 ViewModel 中出现了
Dispatchers或try-catch, 那大概率不是"写法问题",而是 Repository 的职责已经失守。
协程的问题, 往往不是 API 用错了, 而是 边界被模糊了。