18、Flow与依赖注入:Hilt/Dagger中的Flow注入、ViewModel中的Flow注入、Scope管理

依赖注入和Flow,这两个东西放在一起,很多人第一反应就是「哦,用Hilt注入一个Flow呗」。但实际做项目的时候,坑远比想象的多。我见过不少团队,代码里Flow满天飞,但Scope管理一塌糊涂,结果就是泄漏、数据错乱、甚至Crash。

今天咱们就把这块彻底捋清楚。我会结合Hilt和Dagger,聊聊Flow怎么注入、ViewModel里怎么用、以及Scope到底该怎么管。

Hilt/Dagger中的Flow注入:不只是@Inject那么简单

先说说最基础的。你可能会想,Flow不就是个类型吗?直接@Inject不就行了?嗯,理论上可以,但实际项目中,我们往往需要注入的是有生命周期或者有状态的Flow。

举个例子,我有个网络请求的Repository,它暴露了一个Flow:

class UserRepository @Inject constructor(
    private val api: UserApi
) {
    fun getUserFlow(): Flow<User> = flow {
        emit(api.getUser())
    }
}

然后在ViewModel里直接注入这个Repository,再调用getUserFlow()。这没问题,但有个细节——Flow本身是冷是热? 如果是冷Flow,每次collect都会重新执行;如果是热Flow(比如StateFlow、SharedFlow),那就要小心了。

我个人习惯是,在Module里提供热Flow的实例。比如这样:

@Module
@InstallIn(SingletonComponent::class)
object UserModule {

    @Provides
    @Singleton
    fun provideUserStateFlow(
        repository: UserRepository
    ): StateFlow<User> {
        return repository.getUserFlow()
            .stateIn(
                scope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = User()
            )
    }
}

这样,整个应用里注入的StateFlow都是同一个实例,数据一致性好。不过要注意,scope参数别乱传。我见过有人直接把viewModelScope传进去,结果Module是Singleton级别的,View都销毁了scope还在跑,数据就乱了。

⚠️ 避坑指南: 我曾经在一个项目里,把Activity的lifecycleScope传给了stateIn,结果Activity重建时,旧的scope被取消了,新的scope还没创建,中间有段时间Flow是死的。后来改成用WhileSubscribed(5000)加5秒超时,才解决。

ViewModel中的Flow注入:到底该用哪种方式?

ViewModel里用Flow,常见的有两种姿势:

  1. 直接注入Flow对象(比如StateFlow)
  2. 注入Repository,在ViewModel里创建Flow

哪种好?说实话,没有绝对答案。我个人的经验是:

  • 如果这个Flow是全局共享状态(比如用户登录状态、主题设置),直接注入StateFlow更方便。
  • 如果这个Flow是页面级别的数据(比如列表详情),在ViewModel里创建更灵活。

来看个例子。假设我们有个搜索功能,需要根据输入实时搜索:

@HiltViewModel
class SearchViewModel @Inject constructor(
    private val searchRepository: SearchRepository
) : ViewModel() {

    private val _query = MutableStateFlow("")
    val query: StateFlow<String> = _query.asStateFlow()

    val searchResults: StateFlow<List<Result>> = _query
        .debounce(300)
        .flatMapLatest { query ->
            searchRepository.search(query)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun onQueryChanged(newQuery: String) {
        _query.value = newQuery
    }
}

这里我用了flatMapLatest,每次输入变化都会取消上一次的搜索请求。嗯,这个模式在搜索、过滤场景下非常实用。但要注意,flatMapLatest会取消前一个Flow,如果你的Flow里有数据库操作或者网络请求,记得处理好取消逻辑。

💡 小技巧: 我个人习惯在ViewModel里用SharingStarted.WhileSubscribed(5000),而不是EagerlyLazily。为什么呢?因为用户旋转屏幕时,View会短暂销毁,5秒内重建的话,Flow不会重启,体验更流畅。

Scope管理:Flow的生命周期到底归谁管?

这是最容易被忽视的地方。Flow本身没有生命周期,但它的收集操作是有生命周期的。说白了,谁在collect这个Flow,谁就要负责取消它。

在Hilt/Dagger的体系里,Scope管理主要涉及三个层面:

Scope 生命周期 典型场景
@Singleton 应用进程 全局配置、用户登录状态
@ActivityRetainedScoped Activity存活期间(含旋转) ViewModel持有的数据
@ViewModelScoped ViewModel存活期间 页面级数据流

你想想看,如果一个Flow是@Singleton级别的,但你在Activity里collect它,Activity销毁时没有取消收集,会发生什么?没错,内存泄漏。因为Singleton的Flow持有对Activity的引用(比如通过回调),Activity就回收不了了。

我曾经在一个项目里排查内存泄漏,发现一个StateFlow被注入到多个Fragment里,但Fragment销毁时没有取消collect。结果就是,Fragment对象一直被Flow持有,泄漏了。后来我们在BaseFragment里统一处理了collect的取消:

abstract class BaseFragment : Fragment() {

    protected val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

    override fun onDestroyView() {
        super.onDestroyView()
        scope.cancel()
    }
}

当然,现在更推荐用repeatOnLifecycle或者flowWithLifecycle,这些是Android官方提供的生命周期感知方案。

核心原则: Flow的收集范围,不要大于它的提供者范围。比如,@Singleton提供的Flow,可以在任何地方collect,但一定要在合适的时机取消。反过来,@ViewModelScoped提供的Flow,就不要在Application里collect,因为ViewModel销毁后Flow就没了。

一张图看懂Flow与依赖注入的关系

下面这张SVG图,是我自己总结的Flow在Hilt中的完整生命周期。从提供到收集,每一步都对应着Scope的选择。

Flow在Hilt中的生命周期与Scope管理 @Singleton 提供 全局热Flow(StateFlow) @ViewModelScoped 提供 页面级数据流 @ActivityRetainedScoped 旋转不销毁的数据 ViewModel 注入 使用 viewModelScope Fragment 注入 使用 lifecycleScope Service 注入 使用自定义Scope collect / stateIn 自动取消 repeatOnLifecycle 生命周期感知 flowWithLifecycle 暂停/恢复 原则:提供者的Scope ≥ 收集者的Scope,否则可能泄漏或数据不一致

从这张图可以看出来,提供者的Scope一定要大于等于收集者的Scope。比如,@Singleton提供的Flow,可以在ViewModel里收集,因为ViewModel的生命周期比Singleton短。反过来就不行——@ViewModelScoped提供的Flow,如果在Application里收集,ViewModel销毁后Flow就没了,Application那边就会收到异常。

总结一下

Flow和依赖注入结合,说白了就是三件事:

  • 怎么注入:用@Provides还是@Binds?热Flow还是冷Flow?我个人建议,全局状态用热Flow+@Singleton,页面数据用冷Flow+@ViewModelScoped。
  • 怎么收集:ViewModel里用viewModelScope,Fragment/Activity里用repeatOnLifecycle。别偷懒用GlobalScope,我见过血的教训。
  • 怎么取消:谁收集谁取消。如果用了stateIn,记得传正确的scope参数。

嗯,今天就聊这么多。这些经验都是我在实际项目中一点点踩坑踩出来的,希望能帮你少走弯路。

公众号:蓝海资料掘金营,微信deep3321