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 已经被注入,可以直接使用
    }
}
技巧:@AndroidEntryPoint 目前支持 Activity、Fragment、Service、BroadcastReceiver 和 View。如果你在 Compose 中使用,记得在 Compose 所在的 Activity/Fragment 上加该注解。

@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 的核心关键——组件层级决定了依赖的作用域和存活时间。

组件 对应的生命周期 作用域
SingletonComponentApplication 生命周期@Singleton
ActivityComponentActivity 生命周期@ActivityScoped
FragmentComponentFragment 生命周期@FragmentScoped
ViewComponentView 生命周期@ViewScoped
ServiceComponentService 生命周期@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,大家统一用注解,约定大于配置,协作效率提升很明显。

一句话总结:Hilt 是 Dagger 在 Android 上的「官方最佳实践封装」——保留了 Dagger 的编译时安全性,砍掉了繁琐的手动配置。
Hilt 核心概念架构 入口层 @HiltAndroidApp @AndroidEntryPoint 组件层 ApplicationComponent ActivityComponent FragmentComponent ViewComponent ServiceComponent 绑定层 @Inject ← 上一章 📖 返回目录 下一章 →