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

相关推荐
2603_949462102 小时前
Flutter for OpenHarmony社团管理App实战:预算管理实现
android·javascript·flutter
王泰虎4 小时前
安卓开发日记,因为JCenter 关闭导致加载不了三方库应该怎么办
android
2601_949543017 小时前
Flutter for OpenHarmony垃圾分类指南App实战:主题配置实现
android·flutter
2601_949833398 小时前
flutter_for_openharmony口腔护理app实战+知识实现
android·javascript·flutter
晚霞的不甘8 小时前
Flutter for OpenHarmony从基础到专业:深度解析新版番茄钟的倒计时优化
android·flutter·ui·正则表达式·前端框架·鸿蒙
鸟儿不吃草9 小时前
android的Retrofit请求https://192.168.43.73:8080/报错:Handshake failed
android·retrofit
Minilinux20189 小时前
Android音频系列(09)-AudioPolicyManager代码解析
android·音视频·apm·audiopolicy·音频策略
李子红了时9 小时前
【无标题】
android
Android系统攻城狮11 小时前
Android tinyalsa深度解析之pcm_close调用流程与实战(一百零四)
android·pcm·tinyalsa·音频进阶·音频性能实战·android hal
weixin_4111918411 小时前
LifecycleEventObserver和DefaultLifecycleObserver使用
android