摘要
在现代 Android 开发中,Hilt 已经成为了 Google 官方推荐的依赖注入(DI)标准方案。然而,初学者在上手时,往往会被编译器抛出的长串 [Dagger/MissingBinding] 错误搞得晕头转向。本文将复盘一个真实的 Hilt 报错案例,深入剖析 Dagger/Hilt 的绑定原理,并带你一步步搭建一个规范的 DI 模块。
一、 事故现场:那串让人头秃的报错
在构建我们的 Clean MVI Compose 项目时,点击运行,Android Studio 并没有给出丝滑的安装进度条,而是无情地抛出了一段日志:
错误: [Dagger/MissingBinding] com.example.cleanmvicompose.data.user.remote.UserApiService cannot be provided without an @Provides-annotated method.
text
UserApiService is injected at
[...].UserRepositoryImpl(apiService)
UserRepositoryImpl is injected at
[...].RepositoryModule.provideUserRepository(impl)
[...]
翻译成大白话: Hilt 编译器在帮我们组装"零件"时,发现它知道 UserRepositoryImpl 需要一个 UserApiService,但它翻遍了整个"说明书",也没找到哪里能买到或者生产这个 UserApiService。
二、 根源剖析:消失的 @Provides
我们检查了当时的 NetworkModule.kt 代码:
kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
// 这里看起来没问题啊?
fun providerApiService(): UserApiService {
return UserApiService()
}
}
问题点: Dagger/Hilt 是一个**编译时(Compile-time)**框架。它不靠反射,而是靠扫描注解来生成代码。 在上面的代码中,虽然我们写了创建实例的方法,但没有给它贴上 @Provides 标签。对于 Hilt 来说,这个方法只是一个普通的、透明的方法,它看不见,自然也就无法将其加入依赖池。
三、 解决方案:补齐依赖链条
修复方法非常简单,但背后有两个核心注解需要理解:
kotlin
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides // 1. 核心修复:告诉 Hilt 这是一个"供应站"
@Singleton // 2. 作用域:告诉 Hilt 这个东西全局只需要一个
fun provideUserApiService(): UserApiService {
return UserApiService()
}
}
为什么这里要加 @Singleton?
由于我们的 NetworkModule 被安装在了 SingletonComponent 中,这就意味着这个模块的生命周期与整个 App 同步。如果不加 @Singleton,每次有地方需要 UserApiService 时,Hilt 都会重新调用一次构造函数。对于网络请求服务来说,单例化是节省内存和保持连接池复用的最佳实践。
四、 深度进阶:Hilt 模块的设计规范
在实战中,我们通常将 DI 模块按照功能进行分类,而不是全部堆在一起:
- NetworkModule:负责网络请求相关的依赖(Retrofit, OkHttp, Service)。
- DatabaseModule:负责本地存储相关的依赖(Room, DataStore)。
- RepositoryModule:负责将接口抽象与具体实现绑定。
接口与实现的绑定:@Binds vs @Provides
如果你的依赖项是一个接口,且其实现类可以通过 @Inject 构造函数注入,建议使用 @Binds(性能更高,代码更简洁):
kotlin
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}
五、 总结与反思
通过这次报错,我们应该建立起一套 Hilt 排查思维模型:
- 查来源 :提供者(Module)里写
@Provides了吗? - 查去处 :接收者(Constructor)写
@Inject了吗? - 查范围 :
@InstallIn的组件范围覆盖了使用处吗?
依赖注入不是为了增加代码量,而是为了实现"解耦"。 UI 层不再关心 UserApiService 是如何创建的,它只需要伸手去"要"。这种"伸手即得"的优雅,正是我们构建健壮架构的基石。
源码地址
下篇预告: 解决了依赖注入的问题,代码依然会乱作一团?下一篇我们将聊聊 Clean Architecture:如何通过分包策略,为你的项目穿上"防弹衣"。