当 UseCase 开始长期监听,它可能已经不是 UseCase 了

最近在 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 的职责是描述"做什么",而不是在创建时就决定"持续做什么"。

相关推荐
恋猫de小郭14 小时前
Android 限制侧载新进展,谷歌联合国内厂商推验证计划
android·前端·flutter
恋猫de小郭14 小时前
解读 Android 17 全新内存限制,有没有“豁免”后门?
android·前端·flutter
贾艺驰17 小时前
实战Android Framework: 新增一个系统权限
android
alexhilton1 天前
使用Android Archive进行打包
android·kotlin·android jetpack
badhope1 天前
做了几年安卓开发,这些坑我帮你踩过了
android·android studio
逐光老顽童3 天前
Java 与 Kotlin 混合开发避坑指南:30 个真实案例实录
android·kotlin
爱勇宝3 天前
鸿蒙生态的下半场:开发者不只要能开发,还要能赚钱
android·前端·程序员
Yeyu4 天前
刷新一帧的艺术:invalidate / postInvalidate / postInvalidateOnAnimation全解析
android
潘潘潘4 天前
Android OTA 升级原理和流程介绍
android