Hilt 概述 — 从 Dagger 到 Hilt 的依赖注入进化之路
什么是依赖注入?
依赖注入(Dependency Injection,简称 DI)说白了就是「我需要什么,你帮我准备好」。在 Android 开发里,我们经常要在 Activity 里创建 ViewModel、在 Presenter 里调用 Repository,这种「手动 new」的方式不仅代码冗余,而且测试时非常麻烦。
DI 框架做的事很简单:你声明需求,它负责提供实例。这样代码之间就不直接耦合了,单测时也能轻松替换成 Mock 对象。我自己的项目在引入 DI 之前,光是为了给 ViewModel 传递参数就写了上百行工厂方法,后来用 Hilt 重构,瞬间清爽了很多。
从 Dagger 到 Hilt — 我的血泪史
Dagger 在 Android 圈子里成名已久,但说实话,我刚接触它的时候非常痛苦。配置一个最基本的注入环境,你得写 @Component、@Module、@Subcomponent,还要自己管理 Component 的生命周期。我记得第一次用 Dagger 2 搭建项目,光编译就报错了 20 多次,全是「找不到依赖」或者「循环引用」这类问题。
为什么会这么难?因为 Dagger 本身并不了解 Android 组件的生命周期。你得手动创建和销毁 Component,稍微漏掉一处就会导致内存泄漏。更要命的是,项目里多人协作时,每个人对 Dagger 的理解程度不一样,写出来的代码风格五花八门,维护成本直线上升。
Hilt 就是 Google 为了解决这些问题推出的官方方案。它基于 Dagger,但做了三件关键的事:
- 自动管理组件生命周期——与 Android 的 Activity、Fragment 等组件绑定
- 减少样板代码——不用再手写
Component接口 - 编译时严格校验——错误在编译期暴露,不会等到运行时崩溃
从 Dagger 切到 Hilt 之后,我团队里的新人基本一周就能上手,DI 相关的编译错误也少了七成以上。嗯,这就是我要讲这门课的原因——把 Hilt 编译错误彻底讲透。
Hilt 的核心概念
入口注解:@HiltAndroidApp 与 @AndroidEntryPoint
使用 Hilt 的第一步,是在你的 Application 类上加上 @HiltAndroidApp。这个注解会触发 Hilt 的代码生成器,创建出所有依赖注入需要的「基础设施」。
@HiltAndroidApp
class MyApplication : Application() {
// Hilt 会自动生成 Dagger 组件
}
然后,在需要注入依赖的 Android 组件上使用 @AndroidEntryPoint。比如 Activity、Fragment、Service 等。加了注解之后,你就能直接用 @Inject 获取依赖了。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var repository: UserRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repository 已经被注入,可以直接使用
}
}
@Inject:声明依赖「我想要什么」
在构造函数上标记 @Inject,Hilt 就知道如何创建这个类的实例。这是最直接的注入方式。
class UserRepository @Inject constructor(
private val apiService: ApiService
) {
// Hilt 会自动提供 ApiService 实例
}
不过,当你要注入的类是第三方库(比如 Retrofit、OkHttp)时,你没法改它们的构造函数。这时候就需要 @Module 和 @Provides 出场了。
@Module + @Provides + @InstallIn:如何提供第三方依赖
创建一个用 @Module 标记的类,在里面定义用 @Provides 标记的方法。再用 @InstallIn 指定这个模块注入到哪个组件层级。
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl("https://api.example.com")
.build()
}
}
这样,任何需要 Retrofit 的地方,Hilt 都会调用 provideRetrofit() 来提供实例。
Hilt 的组件层次结构
Hilt 内置了一套组件层级,每个组件对应一个 Android 组件的生命周期。这是理解 Hilt 的核心关键——组件层级决定了依赖的作用域和存活时间。
| 组件 | 对应的生命周期 | 作用域 |
|---|---|---|
| SingletonComponent | Application 生命周期 | @Singleton |
| ActivityComponent | Activity 生命周期 | @ActivityScoped |
| FragmentComponent | Fragment 生命周期 | @FragmentScoped |
| ViewComponent | View 生命周期 | @ViewScoped |
| ServiceComponent | Service 生命周期 | @ServiceScoped |
你在 @InstallIn 里指定的组件层级,决定了这个模块里的依赖能存活多久。比如我习惯把全局单例(如 Retrofit、OkHttpClient)放在 SingletonComponent 里,把与 Activity 相关的依赖放在 ActivityComponent 里。这样 Activity 销毁时,对应的依赖也会被释放,避免内存泄漏。
Hilt 的核心优势
讲完概念,说说 Hilt 对比 Dagger 到底好在哪。我整理了一张对比表,你会看得更清楚:
| 对比维度 | Dagger 2 | Hilt |
|---|---|---|
| 组件生命周期管理 | 手动创建/销毁 Component | 自动绑定 Android 生命周期 |
| 样板代码量 | 多(需定义 Component、Subcomponent) | 少(只需注解) |
| 学习曲线 | 陡峭 | 平缓 |
| 编译错误提示 | 模糊、难定位 | 相对清晰,且编译期拦截 |
| 测试支持 | 需要手动替换 Component | 内置 @HiltAndroidTest 支持 |
| 官方维护 | 社区驱动 | Google 官方维护 |
我个人最大的感受是:Hilt 把「依赖注入」从「架构决策」变成了「配置习惯」。以前用 Dagger 时,团队里每次新加一个模块都要讨论 Component 该怎么组织。现在用 Hilt,大家统一用注解,约定大于配置,协作效率提升很明显。