依赖注入: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 的基本路径。
在多模块测试中的应用
你可能会问:这些概念跟回归测试有啥关系?
关系大了。在测试中,你要模拟(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,然后给它写一个简单的测试。当你看到编译成功、测试通过的时候,你就真正理解了这些注解的意义。