第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()
    }
}

你看,这里我提供了OkHttpClientRetrofit两个依赖。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接口,它有UserRepositoryImplMockUserRepository两个实现,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依赖注入流程中的位置:

Hilt 模块与依赖提供流程 @Module 类 @Provides 方法 @Binds 抽象方法 具体对象实例 接口 → 实现类映射 Activity / Fragment / ViewModel @InstallIn 指定组件 @Singleton 控制作用域 方法参数由Hilt自动注入 @Binds 必须用于抽象方法 @Provides 用于具体方法

从图中可以看到,@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模块的基石,理解透了,后面的高级用法才能游刃有余。

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