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还在跑,数据就乱了。
ViewModel中的Flow注入:到底该用哪种方式?
ViewModel里用Flow,常见的有两种姿势:
- 直接注入Flow对象(比如StateFlow)
- 注入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里有数据库操作或者网络请求,记得处理好取消逻辑。
SharingStarted.WhileSubscribed(5000),而不是Eagerly或Lazily。为什么呢?因为用户旋转屏幕时,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与依赖注入的关系
下面这张SVG图,是我自己总结的Flow在Hilt中的完整生命周期。从提供到收集,每一步都对应着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参数。
嗯,今天就聊这么多。这些经验都是我在实际项目中一点点踩坑踩出来的,希望能帮你少走弯路。