Hilt无需显式提供域层UseCase即可注入的原理与设计疑问
我在参考Google I/O官方应用的架构设计实现自有应用时,遇到了Hilt依赖注入相关的困惑:
该项目域层包含基于仓库实现的UseCase组件,我此前开发也会使用这类组件,但之前使用Dagger时需要显式编写模块提供这些UseCase实例。我在Google I/O应用中没有找到任何提供UseCase的Hilt模块,但在自有应用中标注@HiltViewModel的ViewModel里注入UseCase时功能可正常运行:我仅通过Hilt提供了UseCase依赖的仓库等所有下游依赖项,完全没有通过Hilt显式提供UseCase本身。
UseCase基类
abstract class UseCase<in P, R>(private val coroutineDispatcher: CoroutineDispatcher) { suspend operator fun invoke(parameters: P): Resource<R> { return try { withContext(coroutineDispatcher) { execute(parameters).let { Resource.Success(it) } } } catch (e: Exception) { Timber.d(e) Resource.Error(e.toString()) } } @Throws(RuntimeException::class) protected abstract suspend fun execute(parameters: P): R }
UseCase具体实现类
class GetListUseCase @Inject constructor( private val coroutineDispatcher: CoroutineDispatcher, private val remoteRepository: RemoteRepository ): UseCase<ListRequest,ItemsList>(coroutineDispatcher) { override suspend fun execute(parameters: ListRequest): ItemsList{ return remoteRepository.getList(parameters) } }
ViewModel实现
@HiltViewModel class DetailViewModel @Inject constructor( private val getListUseCase: GetListUseCase ): ViewModel() { suspend fun getList(): Resource<ItemsList> { return getListUseCase.invoke(ListRequest(3)) } }
注:原示例代码中此处存在类名与变量名写反、返回值缺失的笔误,已修正为可正常编译的写法
仓库提供示例
@Singleton @Provides fun provideRemoteRepository( api: Api ): RemoteRepository = RemoteRepositoryImpl(api)
远程仓库实现
@ActivityScoped class RemoteRepositoryImpl @Inject constructor( private val api: Api ): RemoteRepository { override suspend fun getList(request: ListRequest): PokemonList { return api.getPokemonList(request.limit, request.offset) } }
注:此处存在作用域冲突问题:绑定的
RemoteRepository为@Singleton作用域,但其实现类RemoteRepositoryImpl标注为@ActivityScoped,实际运行会抛出作用域不匹配异常,需要统一二者作用域。
- 该现象的实现原理是什么?为什么不需要通过Hilt显式提供UseCase?
- 当前无需显式提供即可运行的实现是否存在设计缺陷,是否即使功能正常也需要显式通过Hilt配置UseCase的提供逻辑?
实现原理
这是Hilt(基于Dagger)原生支持的构造函数注入特性,不需要额外编写Module提供实例。
只要满足两个条件,Hilt就会自动生成类的实例创建逻辑,无需手动写@Provides或@Binds配置:
- 待注入的类是具体实现类(非接口、非抽象类)
- 类的构造函数添加了
@Inject注解,且构造函数的所有入参都能被Hilt找到对应的依赖提供规则
你写的GetListUseCase完全满足以上条件:它是具体类,构造函数标了@Inject,依赖的CoroutineDispatcher、RemoteRepository都已经配置了注入规则,所以Hilt可以直接创建它的实例,注入到ViewModel中自然可以正常运行。Google I/O项目中的所有UseCase都是遵循这个写法,自然不需要额外编写Hilt模块提供UseCase实例。
你之前使用Dagger时需要显式提供UseCase,大概率是当时没有给UseCase的构造函数加@Inject注解,才需要手动在Module里写创建逻辑。
是否存在设计缺陷、是否需要显式配置
这种写法完全没有设计缺陷,反而是Hilt/Dagger官方推荐的最优实践,没有任何必要额外编写Hilt模块显式提供UseCase。
只有以下三类场景才需要手动编写Module配置类的提供逻辑:
- 待注入的类型是接口,需要绑定对应的具体实现(比如你代码里的
RemoteRepository是接口,所以需要通过@Binds/@Provides绑定到RemoteRepositoryImpl) - 待注入的类来自第三方库,你无法修改它的源码给构造函数加
@Inject注解 - 实例创建需要额外的初始化逻辑,或者需要配置特殊的作用域、限定符,无法直接通过构造函数注入完成
你的UseCase是自有业务代码里的具体类,构造函数已经标记@Inject,所有依赖都可被Hilt解析,额外写Module提供UseCase属于冗余代码,只会增加后续维护成本,没有任何实际收益。
内容的提问来源于stack exchange,提问作者CJR

