架构避坑:为什么 UseCase 不该启动协程,也不该切线程?

Clean Architecture 的分层规范中,UseCase是承载核心业务逻辑的灵魂。它位于架构的核心圈层,本应是最纯粹、最稳定的存在。

但在实战中,它往往沦为 ViewModel 的"瘦身工具"或 Repository 的"直传传声筒"。这种认知偏差导致项目陷入了"为了分层而分层"的泥潭:

  • Repository 的转发器
  • ViewModel 的工具类
  • 协程调度中心
  • 线程切换器
  • 甚至是"万能胶水层"

结果就是:架构变复杂了,但没有变清晰。

这篇文章专门讲两个最容易踩坑的点:

  • UseCase 不应该启动协程(launch)
  • UseCase 不应该切线程(withContext)
  • 但 UseCase 可以并发(async)

理解这三点,你的架构会立刻清爽很多。 当然在看文章之前,希望你能看看这篇 架构避坑:为什么 Repository 不该启动协程?

🎯 一句话总结 UseCase 的定位

UseCase 是业务流程的描述者,不是协程调度器,也不是线程管理器。

它应该回答的是:

  • "要做什么业务?"

而不是:

  • "在哪个线程执行?"
  • "用哪个 scope 启动?"
  • "怎么调度协程?"

🧭 一、为什么 UseCase 不该启动协程?

1. 职责错位:UseCase 不是执行器

错误示例:

看似方便,但问题巨大:

  • 生命周期不可控
  • 调用方无法等待结果
  • 异常无法向上传递
  • 无法组合多个 UseCase
  • UseCase 变成"自嗨 API"

UseCase 的职责是:

描述业务流程,而不是决定何时执行。

正确方式:

由调用方决定怎么执行:

2. 错误处理会变得一团糟

UseCase 内部 launch 后:

  • 异常不会冒泡
  • 调用方无法捕获
  • 无法统一错误处理

这会让业务流程变得不可控。

3. 可组合性被破坏

如果 UseCase 自己 launch,你无法:

  • 在另一个 UseCase 里等待它
  • 用 async/await 组合多个 UseCase
  • 保证执行顺序
  • 做事务性业务

UseCase 必须是"可等待的",也就是:

  • suspend
  • Flow

🧭 二、为什么 UseCase 不该切线程?

错误示例:

看似合理,但会带来严重问题:

1. 线程策略写死在业务层

UseCase 的核心职责是业务编排 ,而不应关心执行环境

2. UseCase 的复用性下降

如果 UseCase 内部写了 withContext(IO)

  • 在已经是 IO 线程的场景下,会多切一次线程
  • 在 CPU 密集场景下,调度策略不合适
  • 在测试里会变慢、变不稳定

UseCase 越"纯",复用性越高。

3. 破坏了 Repository 的职责

根据 Google 的 Android 架构建议,Repository 应当负责自身的 Main-safety

  • 如果 Repository 已经处理了底层线程,UseCase 的切线程操作就是冗余代码
  • 如果 Repository 没处理,那也应该是 Repository 层的缺陷,而不该由 UseCase 来"打补丁"。

💡 最佳实践建议

核心原则: UseCase 应该是"线程中立"的。

推荐写法:

  1. Repository 层: 负责底层的线程切换(如数据库、网络请求),确保其挂起函数是 Main-safe 的。
  2. ViewModel 层: 负责启动协程,并根据需要指定初始调度器。
  3. UseCase 层: 仅作为纯粹的业务逻辑容器,像流水线一样处理数据。

🏁 总结

在 Clean Architecture 中,每一层都应该对下一层保持信任:

  1. ViewModel 信任 UseCase:只要我调用你,你就会按业务逻辑给我结果,且不会阻塞我。
  2. UseCase 信任 Repository :我不需要关心你是读内存还是读网络,我默认你是 Main-safe 的。
  3. Repository 信任底层驱动:它处理好具体的线程调度。

🧭 三、重点:UseCase 可以并发,但不能启动协程

这是你刚才问的关键点,我把它完整总结进来。

✔ UseCase 可以使用 async

✔ UseCase 可以并发

✔ UseCase 可以用 coroutineScope

❌ UseCase 不能 launch

❌ UseCase 不能决定 scope

❌ UseCase 不能决定线程

💡 精简示例:获取"我的钱包"数据 假设我们需要展示用户余额和对应的优惠券数量:

这里 UseCase 做了什么?

  • 描述业务需要的并发
  • 使用 async/await 组合结果
  • 没有启动协程
  • 没有决定线程
  • 没有依赖外部 scope

这是 UseCase 并发的最佳实践。

调用方:

职责清晰:

  • UseCase:描述业务流程(包括并发)
  • ViewModel:决定怎么执行

下面是数据流转图

🏁 最终总结:UseCase 的"守边计划"

🟢 UseCase 应该做什么?(职责:业务编排)

  • 描述业务流程: 将复杂的业务步骤转化为清晰的代码逻辑。
  • 组合数据源: 作为中间人,协调多个 Repository 提供的数据。
  • 执行业务判断: 处理诸如"权限检查"、"数据过滤"、"结果合并"等纯业务逻辑。
  • 保持中立: 它是纯粹的 Kotlin 逻辑,不依赖 Android 环境,易于测试和复用。

🔴 UseCase 不该做什么?(边界:环境解耦)

  • 禁止切线程: 不硬编码 Dispatchers,确保代码的线程透明度。
  • 禁止启动协程: ioScope,生命周期应由调用者(ViewModel)控制。
  • 禁止处理 UI 状态: 它只返回业务数据或 Result,不关心进度条或弹窗。
  • 禁止管理生命周期: 它应该是无状态的,随调随用,用完即走。

核心信条: 可以并行(async),但不要启动(launch); 可以组合,但不要调度; 可以编排业务,但不要管理线程。

相关推荐
亓才孓11 分钟前
[JDBC]元数据
android
独行soc23 分钟前
2026年渗透测试面试题总结-17(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
金融RPA机器人丨实在智能31 分钟前
Android Studio开发App项目进入AI深水区:实在智能Agent引领无代码交互革命
android·人工智能·ai·android studio
科技块儿32 分钟前
利用IP查询在智慧城市交通信号系统中的应用探索
android·tcp/ip·智慧城市
独行soc1 小时前
2026年渗透测试面试题总结-18(题目+回答)
android·网络·安全·web安全·渗透测试·安全狮
王码码20352 小时前
Flutter for OpenHarmony 实战之基础组件:第二十七篇 BottomSheet — 动态底部弹窗与底部栏菜单
android·flutter·harmonyos
2501_915106322 小时前
app 上架过程,安装包准备、证书与描述文件管理、安装测试、上传
android·ios·小程序·https·uni-app·iphone·webview
vistaup2 小时前
OKHTTP 默认构建包含 android 4.4 的TLS 1.2 以及设备时间不对兼容
android·okhttp
常利兵2 小时前
ButterKnife在Android 35 + Gradle 8.+环境下的适配困境与现代化迁移指南
android
撩得Android一次心动2 小时前
Android LiveData 全面解析:使用Java构建响应式UI【源码篇】
android·java·android jetpack·livedata