第4章:Hilt模块与提供依赖——@Module、@Provides、@Binds注解详解
依赖注入的核心,说白了就是「谁负责把对象造出来,谁负责把对象送过去」。在Hilt的世界里,这个「谁」就是模块(Module)。今天我们就来聊聊Hilt模块里的三个关键注解:@Module、@Provides和@Binds。嗯,这三个注解我几乎每天都在用,但刚接触时也踩过不少坑。
1. @Module:告诉Hilt「我这里有好东西」
@Module注解加在类上,意思是「这个类是一个依赖提供者」。Hilt会扫描所有带@Module的类,从中寻找@Provides或@Binds方法。
我个人习惯把模块按功能拆分,比如网络模块、数据库模块、工具类模块。别把所有东西塞到一个大模块里,否则后期维护会想哭。
关键点:模块必须用@InstallIn指定安装到哪个组件(Component)上。比如@InstallIn(SingletonComponent::class)表示这个模块里的依赖在整个应用生命周期内都可用。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
}
你看,这里我提供了OkHttpClient和Retrofit两个依赖。Hilt会自动把OkHttpClient注入到provideRetrofit的参数里——这就是Hilt的自动推导能力。
2. @Provides:手把手教Hilt造对象
当你要提供的对象不是接口,而是一个具体的类,或者这个对象的构造方式比较复杂(比如需要配置、需要调用Builder),就用@Provides。
我曾经在一个项目里需要提供加密工具类,它的构造函数需要传入密钥和算法类型。这种场景用@Provides最合适——你可以在方法里写清楚怎么创建它。
@Module
@InstallIn(SingletonComponent::class)
object SecurityModule {
@Provides
@Singleton
fun provideAesEncryptor(): AesEncryptor {
val key = "my-secret-key-12345".toByteArray()
return AesEncryptor(key, "AES/GCM/NoPadding")
}
}
注意:@Provides方法可以带参数,Hilt会从依赖图中自动查找并注入这些参数。但参数类型必须在依赖图中存在,否则编译会报错。
小技巧:如果提供的是单例,记得加上@Singleton注解。否则每次注入都会创建一个新实例,这在某些场景下可能不是你想要的行为。
3. @Binds:接口与实现类的「红娘」
@Binds用于将接口绑定到它的实现类。它必须放在抽象方法上,而且方法只能有一个参数——就是实现类的实例。
为什么需要@Binds?因为Hilt不知道接口该用哪个实现。你想想看,如果有一个UserRepository接口,它有UserRepositoryImpl和MockUserRepository两个实现,Hilt怎么选?这时候就需要你显式告诉它。
interface UserRepository {
fun getUser(): User
}
class UserRepositoryImpl @Inject constructor(
private val api: ApiService
) : UserRepository {
override fun getUser(): User {
return api.fetchUser()
}
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: UserRepositoryImpl
): UserRepository
}
这里有个细节:UserRepositoryImpl的构造函数用了@Inject,这样Hilt就知道怎么创建它。而@Binds方法只是把实现类「映射」到接口上。
我曾经踩过的坑:把@Binds和@Provides混在同一个模块里。Hilt要求一个模块要么全是抽象方法(@Binds),要么全是具体方法(@Provides),不能混用。如果你需要同时提供接口绑定和具体对象,请拆成两个模块。
4. @Binds vs @Provides:什么时候用哪个?
| 场景 | 推荐注解 | 原因 |
|---|---|---|
| 接口 → 实现类 | @Binds |
更简洁,性能更好(编译期生成代码) |
| 第三方库的类(如OkHttpClient) | @Provides |
你无法修改它的构造函数 |
| 需要复杂配置的对象 | @Provides |
可以在方法里写配置逻辑 |
| 需要多个构造参数 | @Provides |
参数由Hilt自动注入,你只需关注创建逻辑 |
| 自己写的类,且构造函数简单 | @Inject构造函数(无需模块) |
最简洁,Hilt直接使用构造函数 |
说白了,能用@Binds就用@Binds,因为它更「声明式」,代码量也更少。但遇到第三方类或复杂创建逻辑,@Provides是你的不二之选。
5. 模块的组织与最佳实践
我见过不少项目把所有@Provides方法堆在一个模块里,结果模块文件上千行。嗯,这其实不太好。我个人建议按功能领域拆分模块:
- NetworkModule:提供OkHttp、Retrofit、日志拦截器等
- DatabaseModule:提供Room数据库、DAO等
- RepositoryModule:提供各种Repository接口的绑定
- UtilModule:提供工具类、SharedPreferences等
每个模块只负责一个「领域」,这样当你想替换某个依赖时,只需要修改对应的模块,不会影响到其他部分。
核心原则:模块应该是「高内聚、低耦合」的。一个模块只做一件事,并且做好。
6. 知识体系总览
下面这张图帮你理清@Module、@Provides、@Binds之间的关系,以及它们在整个Hilt依赖注入流程中的位置:
从图中可以看到,@Module是入口,它包含两种方法:@Provides直接产出具体对象,@Binds建立接口与实现的映射。最终这些依赖都会被注入到Activity、Fragment或ViewModel中。
7. 避坑指南
最后分享几个我实际踩过的坑,希望能帮你少走弯路:
- 模块没加@InstallIn:Hilt会直接忽略这个模块,编译不报错,但运行时注入会失败。嗯,这个坑我至少花了半天才定位到。
- @Binds方法不是抽象方法:编译器会直接报错,因为
@Binds只能用在抽象方法上。 - 同一个接口绑定了多个实现:Hilt会报「依赖冲突」错误。这时候需要用
@Qualifier来区分,这个我们后面章节会详细讲。 - @Provides方法返回类型不精确:比如返回
OkHttpClient但方法声明返回Object,Hilt会按Object类型注册,导致注入OkHttpClient时找不到依赖。记得返回类型要精确。
我的习惯:每次写完模块,我都会在编译后检查一下Hilt生成的Dagger组件代码(在build/generated/source/kapt/目录下),看看依赖是不是按预期生成的。这能帮你提前发现很多问题。
好了,关于@Module、@Provides和@Binds就聊到这里。这三个注解是Hilt模块的基石,理解透了,后面的高级用法才能游刃有余。