依赖注入:Dagger 与 Hilt 入门

好,我们进入正题。到了多模块项目这个阶段,依赖注入就不再是“可选项”了——它几乎是必须的。我个人习惯是把依赖注入看作一座桥梁,负责把各模块需要的零件(也就是依赖)精准地送到它们手里。Dagger 和它的“简化版” Hilt,就是这桥的搭建工兵。

一句话总结:依赖注入(DI)让模块之间不再“硬耦合”,而是通过外部容器来管理依赖的创建和生命周期。这在分离测试时你就能体会到它的好了。

Hilt 与 Dagger 的关系

聊到 Hilt,就绕不开 Dagger。很多新手会问:“我要学哪个?” 我的看法是:Hilt 就是官方封装过的 Dagger。它俩的关系有点类似 Retrofit 和 OkHttp。

Dagger 是一个纯编译期的注解处理器,它通过生成代码来解决依赖注入问题。能力强,但配置繁琐。你想想看,一个中型项目里光 Component、Module、Scope 这些概念就能让新人绕晕。我在项目中遇到过不止一次,因为 Component 层级设计不合理,导致编译出错的窘境。

Hilt 的出现就是为了解决这个痛点。它是在 Dagger 之上的一套标准化配置。说白了,Hilt 预定义了 Android 组件的生命周期和绑定方式,你只需要在 Application、Activity、Fragment 上加上几个注解,它就自动帮你把 Dagger 的 Component 搭好了。

特性 Dagger Hilt
配置复杂度 高(需手动创建 Component) 低(自动生成)
Android 集成 需要额外扩展库 天生支持 Activity/Fragment
编译时间 相对较快 稍慢(但可接受)
适用场景 大型、高度定制化 大多数项目,尤其是多模块

如果你要做多模块回归测试,我会强烈推荐你从 Hilt 开始。它能让你把主要精力放在业务和测试上,不用在依赖注入的“基建”上浪费太多时间。

个人建议:就算你现在用着纯 Dagger,也不妨了解一下 Hilt 的思路。因为它的组件树模型(Component Hierarchy)对于理解测试中的模拟替换非常有帮助。

核心注解概览

接下来是硬核部分。Hilt/Dagger 里有几个关键的注解,它们就像是不同角色的“建筑工人”。搞清楚它们的职责,你写代码时就能游刃有余。

@Inject

这个注解有两个使用场景:标记构造函数 和 标记字段。

// 场景一:标记构造函数——告诉 Hilt/Dagger “这个类可以被它自己创建”
class UserRepository @Inject constructor(
    private val apiService: ApiService
) {
    fun fetchUser(): User { ... }
}

// 场景二:标记字段——告诉 Hilt/Dagger “请给我一个实例”
class MainViewModel : ViewModel() {
    @Inject
    lateinit var repository: UserRepository
}

嗯,这里要注意:@Inject 标记构造函数时,这个类的创建权就交给了 DI 框架。标记字段时,它会在合适的时机(比如 Activity 创建时)自动注入进来。

@Module 与 @Provides

有时候你没法用 @Inject 注解构造函数。比如第三方库 Retrofit 或者 Room 的实例。这时候你需要一个 Module——它是一个生产依赖的“工厂车间”。

@Provides 则定义在这个车间里,具体怎么生产这个依赖。

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .build()
    }

    @Provides
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

避坑指南:我曾经在一个多模块项目里,不小心在子模块里漏了 @InstallIn 注解,结果编译死活过不去。排查了半天才发现是 Hilt 不知道要把这个 Module 装在哪个生命周期里。记住:每个 Module 必须通过 @InstallIn 指明它的作用范围。这在多模块环境下尤为关键。

@Component(Dagger 核心)

如果你是纯 Dagger 用户,@Component 是核心中的核心。它就像是一张“蓝图”,告诉 Dagger 哪些 Module 能生成依赖,以及最终要把依赖注入到哪个目标对象里。

但是在 Hilt 里,你基本不用写 @Component。Hilt 已经预定义好了:SingletonComponent(与 Application 同生命周期)、ActivityComponent、FragmentComponent 等等。你只用 @HiltAndroidApp 和 @AndroidEntryPoint 就够了。

// Hilt 方式:不需要手动定义 Component
@HiltAndroidApp
class MyApplication : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var repository: UserRepository
}

这比手写 Component 省事多了,对吧?

知识结构一览

为了让你对整个流程有个直观印象,我画了一张图。它展示了从 Module 到依赖最终被注入到 Activity 的基本路径。

Hilt/Dagger 依赖注入核心流程 @Module @Provides @Inject (构造函数) (被注入类本身) @Component @AndroidEntryPoint (Activity / Fragment) @Inject (字段) Hilt 自动生成 不需要手动编写

在多模块测试中的应用

你可能会问:这些概念跟回归测试有啥关系?

关系大了。在测试中,你要模拟(Mock)掉真实的后端请求或者数据库。Hilt 提供了 @HiltAndroidTest 和 @BindValue 来轻松替换模块里的真实依赖。你不需要再手动创建复杂的 Dagger Component 来注入模拟对象。

@HiltAndroidTest
class LoginViewModelTest {

    // 用 @BindValue 直接在测试类里替换真实依赖
    @BindValue
    val mockApi: ApiService = mock()

    @Test
    fun testUserLogin() {
        // 直接使用被注入的 ViewModel,但它的依赖已经被 mock 替换了
    }
}

这里有个小窍门:在多模块项目中,建议每个功能模块都建立一个专属的测试 Module。这个 Module 里不提供任何真实实现,只提供 Mock 依赖。测试环境运行时,让 Hilt 优先加载这个测试 Module。这样你就能在不影响其他模块的前提下,精确控制被测试单元的依赖。

好了,关于依赖注入的门槛,我们就聊到这儿。记得自己动手在项目里加上 Hilt,然后给它写一个简单的测试。当你看到编译成功、测试通过的时候,你就真正理解了这些注解的意义。