架构避坑:为什么 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); 可以组合,但不要调度; 可以编排业务,但不要管理线程。

相关推荐
Mr -老鬼2 小时前
Android studio 最新Gradle 8.13版本“坑点”解析与避坑指南
android·ide·android studio
xiaolizi56748910 小时前
安卓远程安卓(通过frp与adb远程)完全免费
android·远程工作
阿杰1000110 小时前
ADB(Android Debug Bridge)是 Android SDK 核心调试工具,通过电脑与 Android 设备(手机、平板、嵌入式设备等)建立通信,对设备进行控制、文件传输、命令等操作。
android·adb
梨落秋霜10 小时前
Python入门篇【文件处理】
android·java·python
遥不可及zzz13 小时前
Android 接入UMP
android
Coder_Boy_15 小时前
基于SpringAI的在线考试系统设计总案-知识点管理模块详细设计
android·java·javascript
冬奇Lab15 小时前
【Kotlin系列03】控制流与函数:从if表达式到Lambda的进化之路
android·kotlin·编程语言
冬奇Lab15 小时前
稳定性性能系列之十二——Android渲染性能深度优化:SurfaceFlinger与GPU
android·性能优化·debug
冬奇Lab17 小时前
稳定性性能系列之十一——Android内存优化与OOM问题深度解决
android·性能优化