前言
很早之前读了郭神写的一篇讲解依赖注入的文章:Jetpack新成员,一篇文章带你玩转Hilt和依赖注入,犹记得当时看完如同醍醐灌顶,不得不盛赞郭神的好文。
但隔了几年后,读 nowinandroid 项目源码时,看到项目中使用的 Hilt 还是感觉有些生疏,我知道是因为我没有真正使用过也没有总结过这项技术,所以我决定还是跟着郭神的教程写一遍,毕竟"纸上得来终觉浅,绝知此事要躬行"。很多技术看完以为自己理解了,但是没有经过自己梳理,并且在梳理过程中反复思考,加深理解这个过程,总是容易不得要领。
郭神的文章比我写的自然要好得多,感兴趣的读者建议直接去看原文,本文只是笔者自己的总结。
诶,别骂人哦,不是洗稿,是致敬,致敬!
什么是依赖注入
依赖注入(Dependency Injection,简称 DI),是软件工程中的一个概念。使用依赖注入可以让项目更好的解耦。
依赖注入这个词读起来就挺费解:拆开来看,依赖,注入,两个词都有点陌生;合起来看,依赖注入,更觉得有点高大上,好像是个很难懂的东西。
我记得之前听到一个说法:所谓学习,就是掌握一些新概念。学习依赖注入也是一样,很重要的一点就是掌握这个词语的概念:什么叫依赖,为什么要注入。
"依赖"可以表示自己无法做到,需要依靠别人。比如孩子生活无法自理,需要依赖父母。也可以表示自己使用的工具,比如我虽然可以手搓钻木取火(这个例子有点吹牛,其实并不可以),但我使用打火机可以更方便地生火,所以我生火时依赖打火机。
在程序中,"依赖"表示"我做事需要用到谁"。比如我的项目需要用到 OkHttp,所以我在 gradle 的 dependencies 中配置了 OkHttp 这一项依赖。
再比如我这一段代码需要用到 TimeZone 这个类,所以可以说这个类依赖 TimeZone。
郭神举了一个卡车送货的例子,一辆卡车,需要配送两台电脑,代码如下:
scss
class Truck {
val computer1 = Computer()
val computer2 = Computer()
fun deliver() {
loadToTruck(computer1)
loadToTruck(computer2)
beginToDeliver()
}
}
这段代码中,卡车使用了 Computer 类,所以卡车依赖 Computer。
如果卡车需要配送蔬菜,那么它就要依赖蔬菜类,如果要配送手机,那么它就要依赖手机类,这就产生了耦合。
但不这么写还能怎么写呢?我想到的一种写法是创建一个货物工厂类 CargoFactory,由它负责生产货物,然后卡车从工厂里把货运走即可,这样卡车就无需关心这是什么货物,也无需关心这些货物是怎么生产出来的。
有的读者要问了:这样卡车就依赖于货物工厂了啊,不还是有耦合吗?
确实,但好处是它只依赖于货物工厂了,不会依赖非常多的类。
"注入"的意思是"传入",也就是"把需要的东西传进去"。
合起来理解,"依赖注入"的意思就是:这个类有依赖(需要别的对象),但是它不会自己去创建这些依赖,而是让外部把依赖传给它(注入进去)。
所以依赖注入框架就类似于一个货物工厂,帮你创建对象。使用了 Hilt 的代码大概长这样:
Kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Text(text = "Hello Android!", modifier = Modifier.padding(innerPadding))
}
}
}
truck.deliver()
}
}
class Truck @Inject constructor() {
fun deliver() {
println("Truck is delivering goods.")
}
}
可以看到,MainActivity 中有一个 truck 变量,在 truck 变量上加了一个 @Inject 注解后,这个对象就被注入进来了,后续直接使用即可。MainActivity 无需知道这个对象谁创建的,怎么创建的。
那么,到底是谁在创建 Truck 对象呢?通过给 Truck 类的构造函数添加 @Inject 注解,Hilt 就会用这个构造函数创建对象了。
有的读者又要问了,这样和我直接 new 出一个对象有什么区别?直接 new 一个还要更简洁一些。
确实,目前这个例子来看,直接 new 出来会更简洁一些。但是当构造一个对象比较复杂时,通常用依赖注入会更方便。这个优势和工厂模式的优势是一样的,比如构建一辆货车需要引擎、轮胎、外壳等等,使用它的地方直接注入即可,无需关心它是如何构造的。
所以简单来说,依赖注入有两大优势:一是将对象的创建和使用分离开来,达到解耦的目的。二是随着对象的创建越来越复杂,使用时依然能够保持简洁。
使用 Hilt
gradle/libs.versions.toml 添加版本、库、插件
导入依赖:
Kotlin
[versions]
hilt = "2.60.1"
ksp = "2.3.4"
[libraries]
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hilt" }
hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hilt" }
[plugins]
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
根目录 build.gradle.kts 里声明别名
Kotlin
plugins {
alias(libs.plugins.hilt.android) apply false
alias(libs.plugins.ksp) apply false
}
app 模块 app/build.gradle.kts 应用插件并添加依赖
Kotlin
plugins {
alias(libs.plugins.hilt.android)
alias(libs.plugins.ksp)
}
dependencies {
implementation(libs.hilt.android)
ksp(libs.hilt.compiler)
}
在 Application 中添加 @HiltAndroidApp 注解:
Kotlin
@HiltAndroidApp
class MyApplication : Application()
到这里,准备工作就完成了,Hilt 已经被添加到项目中,并且初始化完成了。
然后修改 MainActivity:
Kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Text(text = "Hello Android!", modifier = Modifier.padding(innerPadding))
}
}
}
truck.deliver()
}
}
class Truck @Inject constructor() {
fun deliver() {
println("Truck is delivering goods.")
}
}
运行程序,输出如下:
csharp
Truck is delivering goods.
这里用到了一个 @AndroidEntryPoint 注解,表示这个 Activity 中需要用到依赖注入。Hilt 只支持 6 个入口点:
- Application
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
带参数的依赖注入
如果构造函数需要带参数,比如添加一个 Driver 对象, 那么可以给 Driver 类也加上依赖注入:
Kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyApplicationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Text(text = "Hello Android!", modifier = Modifier.padding(innerPadding))
}
}
}
truck.deliver()
}
}
class Truck @Inject constructor(val driver: Driver) {
fun deliver() {
println("Truck is delivering goods. driver: $driver")
}
}
class Driver @Inject constructor()
运行程序,输出如下:
kotlin
Truck is delivering goods. driver: com.example.myapplication.Driver@f982156
只有 Truck 的构造函数中所依赖的其他对象都支持依赖注入了,Truck 类才能被依赖注入。
那么我们很快就能想到一个问题:如果我依赖的是第三方类呢?比如 Truck 需要一个 TimeZone 对象,这是 java.util 包下的对象,我怎么去给它的构造函数添加 @Inject 注解呢?
答案是使用 @Provides 注解。
第三方类的依赖注入
给 Truck 类添加一个 timeZone 参数,通过 @Provides 注解实现 TimeZone 类的依赖注入:
Kotlin
class Truck @Inject constructor(val timeZone: TimeZone) {
fun deliver() {
println("Truck is delivering goods. timeZone: ${timeZone.displayName}")
}
}
@Module
@InstallIn(ActivityComponent::class)
class TimeModule {
@Provides
fun provideTimeZone(): TimeZone {
return TimeZone.getDefault()
}
}
这里定义了一个 TimeModule 类,这个类用来包装 DI 规则,名字可以随意取,通常按照功能划分,叫做 XxxModule。
添加了 @Module 注解,告诉 Hilt 这是这是一份"依赖提供规则"。
@InstallIn 注解就有点意思了,直译为"安装到",表示这个类里的规则可以在哪些地方使用。有这些常用的值:
SingletonComponent::class(应用级)ActivityRetainedComponent::class(AI 老师说这个表示:配置变更保留,常给 ViewModel 上游依赖,我暂时不知道什么意思)ViewModelComponent::classActivityComponent::classFragmentComponent::classViewComponent::classViewWithFragmentComponent::classServiceComponent::class
如果需要注入多个 components,可以写多个:
Kotlin
@InstallIn(ActivityComponent::class, FragmentComponent::class)
运行程序,输出如下:
csharp
Truck is delivering goods. timeZone: Eastern Standard Time
AI 老师说只有当这组 DI 规则(provider/bindings)确实要在多个层级都可见且语义一致时才安装到多个 Component。
更常见做法是:
- 放到上层(如
SingletonComponent)让下层自然可用,或 - 拆成多个模块,各装各的层级,边界更清晰。
这里我目前暂时不太理解,应该要实战一下才能理解透彻。
接口的依赖注入
以引擎接口为例:
Kotlin
interface Engine {
fun start()
}
class GasEngine @Inject constructor(): Engine {
override fun start() {
println("Gas engine started.")
}
}
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
}
这里定义了 Engine 接口,定义了 GasEngine 实现类。然后定义了 EngineModule,通过 @Binds 注解实现接口的依赖注入。
bindEngine 方法中传入的参数的类型,就是注入的类型。
使用方式:
Kotlin
class Truck @Inject constructor(val timeZone: TimeZone) {
@Inject
lateinit var engine: Engine
fun deliver() {
engine.start()
}
}
运行程序,输出如下:
erlang
Gas engine started.
需要注意的是,如果一个接口有多个实现,我们不能通过像这样直接定义多个 @Binds 方法实现注入。比如说,假设 Engine 接口还有一个 ElectricEngine 实现类,也通过这种方式注入,那么这一行代码就会产生歧义:
Kotlin
@Inject
lateinit var engine: Engine
因为 Hilt 不知道应该注入哪一种具体的实例。在编译阶段就会报错(error: [Dagger/DuplicateBindings] xxx is bound multiple times)。
这种情况需要定义新的注解去解决。
给相同类型注入不同的实例
以货物接口为例:
Kotlin
interface Cargo {
fun description()
}
class FoodCargo @Inject constructor(): Cargo {
override fun description() {
println("This is food cargo.")
}
}
class PhoneCargo @Inject constructor(): Cargo {
override fun description() {
println("This is phone cargo.")
}
}
这里我们定义了一个 Cargo 接口,添加了 FoodCargo 和 PhoneCargo 两种实现。想要实现依赖注入,需要这样写:
Kotlin
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindFoodCargo
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindPhoneCargo
@Module
@InstallIn(ActivityComponent::class)
abstract class CargoModule {
@BindFoodCargo
@Binds
abstract fun bindFoodCargo(foodCargo: FoodCargo): Cargo
@BindPhoneCargo
@Binds
abstract fun bindPhoneCargo(phoneCargo: PhoneCargo): Cargo
}
首先定义两个注解,关于注解的知识我很早之前写过一篇文章:Andrdoid 注解和 meta-data (Kotlin)。
通过 @Qualifier 告诉 Hilt,这是接口的其中一个实例。@Qualifier 就是用来给同一个接口的不同实现进行分类的。
使用方式:
Kotlin
class Truck @Inject constructor(val timeZone: TimeZone) {
@BindFoodCargo
@Inject
lateinit var foodCargo: Cargo
@BindPhoneCargo
@Inject
lateinit var phoneCargo: Cargo
fun deliver() {
foodCargo.description()
phoneCargo.description()
}
}
运行程序,输出如下:
csharp
This is food cargo.
This is phone cargo.
作用域
前文提到 @InstallIn 中,有这些常用的值:
SingletonComponent::class(应用级)ActivityRetainedComponent::classViewModelComponent::classActivityComponent::classFragmentComponent::classViewComponent::classViewWithFragmentComponent::classServiceComponent::class
并且前文提到说这代表的是这个类里的规则可以在哪些地方使用。
更准确地说,它代表的是这个依赖可以被注入到哪些生命周期更短或同级的地方,以及它能活多久。
贴一张官方文档上的图(这不是我做的图,但我不知道掘金怎么去水印,掘金的水印让我烦不胜烦,如果有读者知道,求告知):

借用郭神的话来讲,就是对某个类声明了某种作用域注解之后,这个注解的箭头所能指到的地方,都可以对该类进行依赖注入,同时在该范围内共享同一个实例。
比如 @Singleton 注解的箭头可以指向所有地方,相当于是一个全局的单例。
而 @ServiceScoped 注解的箭头无处可指,所以只能限定在 Service 中使用。
预置 Qualifier
注入 Application 的 Context:
Kotlin
class TestApplicationContext @Inject constructor(@param:ApplicationContext val context: Context) {
fun printContext() {
println("Context: $context")
}
}
注入 Activity 的 Context:
Kotlin
class TestActivityContext @Inject constructor(@param:ActivityContext val context: Context) {
fun printContext() {
println("Activity Context: $context")
}
}
ViewModel 的依赖注入
用上文学到的知识,给 ViewModel 进行依赖注入,需要这样写:
Kotlin
class Repository @Inject constructor() {
fun getData(): String {
return "Data from Repository"
}
}
Kotlin
@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
fun getData(): String {
return repository.getData()
}
}
kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var viewModel: MyViewModel
}
这种方式虽然可以正常工作,但是它改变了我们获取 ViewModel 的常见方式。所以 Hilt 对 ViewModel 提供了一种独立的依赖注入方式。
首先,导入两个依赖:
toml
[versions]
hiltLifecycleViewmodel = "1.4.0"
[libraries]
androidx-hilt-lifecycle-viewmodel = { module = "androidx.hilt:hilt-lifecycle-viewmodel", version.ref = "hiltLifecycleViewmodel" }
androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "hiltLifecycleViewmodel" }
Kotlin
dependencies {
implementation(libs.androidx.hilt.lifecycle.viewmodel)
ksp(libs.androidx.hilt.compiler)
}
然后使用 @HiltViewModel 注解:
Kotlin
@HiltViewModel
class MyHiltViewModel @Inject constructor(val repository: Repository) : ViewModel() {
fun getData(): String {
return repository.getData()
}
}
然后就可以正常使用 ViewModel 了:
Kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val hiltViewModel: MyHiltViewModel by lazy { ViewModelProvider(this).get(MyHiltViewModel::class.java) }
}
自定义入口点
如果需要自定义入口点,可以用 @EntryPoint 注解,搭配这样的代码:
ini
val entryPoint = EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java)
但这个功能不常用,本文不再讲解。