最近在 review 代码时,遇到一类非常典型但隐蔽的问题:一些名为 UseCase 的类,并不是在"被调用时执行逻辑",而是在创建的瞬间就开始消费数据流。
表面上,它只是"订阅了 Repository 的 Flow 并做状态同步";但从架构语义上看,它已经悄悄变成了一个隐藏的常驻后台任务。
这个变化非常关键,但往往不会引发任何报错,因此很容易长期潜伏在代码库里。
一个很常见但有问题的写法
kotlin
class FeatureAvailabilityUseCase(
private val repo: FeatureRepository,
private val workerScope: CoroutineScope,
) {
var isFeatureAvailable: Boolean = false
private set
init {
workerScope.launch {
repo.observeFeatureEnabled()
.collect { enabled ->
isFeatureAvailable = enabled
}
}
}
}
这种写法在很多项目中都能见到。
从功能上看没有问题:
- Repository 提供 Flow
- UseCase 负责消费并转换状态
- Scope 保证协程运行
但问题不在"能不能跑",而在于职责语义已经被悄悄改变了。
核心问题:UseCase 不应该在构造阶段产生副作用
一个语义清晰的 UseCase,通常应该满足一个基本约定:
行为由调用触发,而不是由对象创建触发
典型形式是:
kotlin
suspend operator fun invoke(): Result<T>
或:
kotlin
fun observe(): Flow<T>
共同特点是:惰性执行(lazy)
- 不调用,就不会发生任何行为
- 生命周期由调用方控制
- 副作用集中在"入口函数",而不是构造阶段
而 init { launch { collect } } 打破了这个约定:
对象刚创建,行为已经开始运行
这会直接导致一个架构层面的变化: UseCase 从"业务执行单元",变成了"自动启动的订阅器"。
Cold Flow 被"隐式热化"
Repository 通常返回的是冷流:
kotlin
fun observeFeatureEnabled(): Flow<Boolean>
冷流的语义是:
谁 collect,什么时候 collect,才真正开始执行
但在 UseCase 的 init 中 collect,会发生一个关键变化:
text
Flow 冷流语义仍然存在
但生命周期被 UseCase 强行接管
结果就是:
- 数据生产链路在对象创建时被启动
- 订阅行为不再由 UI/ViewModel 决定
- Flow 的"惰性"被破坏成"隐式常驻"
更严重的问题:生命周期完全不透明
这种写法最大的问题不是性能,而是不可控性。
1. 谁来 cancel?
kotlin
workerScope.launch { ... }
关键问题变成:
workerScope是谁提供的?- 它的生命周期和 UseCase 是否一致?
- UseCase 被释放时,这个协程是否会自动结束?
这些问题都没有在结构上表达出来,只能"靠约定",而约定往往是不可靠的。
2. UseCase 的语义已经丢失
正常 UseCase:
执行一次任务 → 返回结果 → 结束
当前写法:
创建对象 → 启动协程 → 永久 collect → 常驻内存
这已经不是 UseCase,而是:
- BackgroundMonitor
- 或某种隐式 Service
只是名字还叫 UseCase。
状态缓存的隐性问题
kotlin
var isFeatureAvailable: Boolean = false
这个设计看似简单,但实际上有三个结构性问题:
1. 状态不可观察
它不是 StateFlow,UI 无法订阅变化,只能"被动读取"。
2. 初始值语义不明确
text
false = 真不可用?
false = 还没加载?
调用方无法区分"状态未初始化"和"真实业务状态"。
3. 线程安全隐患
写发生在协程线程,读可能在主线程:
- 没有同步机制
- 没有可见性保证
- 本质上是竞态数据
本质问题:职责三合一
这个 UseCase 实际上同时做了三件事:
| 职责 | 正常归属 |
|---|---|
| 启动协程 | ViewModel / Application |
| 订阅 Flow | ViewModel / Repository |
| 持有状态 | Repository / State Holder |
而 UseCase 在 Clean Architecture 中应该只负责一件事:
表达业务动作(what to do)
不是:
我创建后就持续在后台运行什么(what keeps running)
更推荐的写法
方案一:保持 UseCase 纯粹(推荐默认)
kotlin
class FeatureAvailabilityUseCase(
private val repo: FeatureRepository,
) {
fun observe(): Flow<Boolean> = repo.observeFeatureEnabled()
}
特点:
- 无副作用
- 无 scope
- 生命周期完全外部控制
- 最符合 Clean Architecture
方案二:在 ViewModel 中处理状态流
kotlin
val isFeatureAvailable: StateFlow<Boolean> =
repo.observeFeatureEnabled()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = false
)
优势:
- 生命周期清晰
- 自动取消订阅
- UI 可直接观察
- 热流语义显式
一个简单判断标准
可以用一句话判断它是否还是 UseCase:
如果一个 UseCase 在 init 中启动协程并 collect Flow,它就已经不再是 UseCase。
因为它不再描述"动作",而是在描述"长期运行的状态"。
总结
这个问题的核心不是技术实现,而是架构语义的偏离。
当 UseCase 在 init 中 collect Flow 时,它发生了三个变化:
- 从"被调用执行" → "创建即运行"
- 从"短生命周期动作" → "长生命周期订阅器"
- 从"业务表达层" → "后台状态守护者"
它不会 crash,不会报错,但是会有隐藏很深的bug。
UseCase 的职责是描述"做什么",而不是在创建时就决定"持续做什么"。