为什么需要依赖注入?
依赖注入(Dependency Injection,简称 DI)是一种设计模式,我们为什么需要它呢?
为了实现类之间的解耦,以及依赖关系的管理。
高耦合
我们来看一个人和咖啡机的例子:
kotlin
// 只能做美式咖啡的咖啡机
class SpecificCoffeeMachine {
init {
println("(An expensive SpecificCoffeeMachine was built...)")
}
fun makeAmericano() {
println("Making... Americano") // ☕️
}
}
// 人
class Person {
// 硬编码了咖啡机的创建
// Person 和 SpecificCoffeeMachine 被耦合在了一起
private val myCoffeeMachine: SpecificCoffeeMachine = SpecificCoffeeMachine()
fun startMorning() {
println("Good morning, I need coffee...")
myCoffeeMachine.makeAmericano()
println("The coffee is finished and work is done.")
}
}
fun main() {
val bob = Person()
bob.startMorning()
}
看似没什么问题,但如果有一天,Bob 想喝拿铁了,我们只能修改 Person 类的代码,去强行修改它的内部结构。
kotlin
// 只能做拿铁的咖啡机
class LatteMachine {
fun makeLatte() {
println("Making... Latte") // 🥛☕️
}
}
// 人
class Person {
private val myCoffeeMachine: LatteMachine = LatteMachine()
fun startMorning() {
println("Good morning, I need coffee...")
myCoffeeMachine.makeLatte() // 甚至调用的方法名也需要修改
println("The coffee is finished and work is done.")
}
}
如果有 100 个类使用了 SpecificCoffeeMachine,我们就得去修改 100 个类。
并且无法对 Person 的 startMorning() 进行单元测试。
在创建 Person 对象时,其内部一定会创建一个 SpecificCoffeeMachine 实例,如果 SpecificCoffeeMachine 在创建时存在复杂操作,单元测试就会变慢。测试失败时不清楚是谁的原因,不清楚 startMorning() 内部是否真的调用了 makeAmericano(),单元测试变得无法验证。
低耦合
我们使用依赖注入来解耦,现在 Person 不主动创建咖啡机,而是由外部创建。
kotlin
interface CoffeeMaker {
fun makeCoffee()
}
class AmericanoMachine : CoffeeMaker {
override fun makeCoffee() {
println("Making... Americano") // ☕️
}
}
class LatteMachine : CoffeeMaker {
override fun makeCoffee() {
println("Making... Latte") // 🥛☕️
}
}
class Person(
// 咖啡制造机由外部注入
private val coffeeMaker: CoffeeMaker
) {
fun startMorning() {
println("Good morning, I need coffee...")
coffeeMaker.makeCoffee()
println("The coffee is finished and work is done.")
}
}
fun main() {
// Bob 想喝美式了
val americanoDay = Person(AmericanoMachine())
americanoDay.startMorning()
println("--- Day 2 ---")
// Bob 想喝拿铁了
val latteDay = Person(LatteMachine())
latteDay.startMorning()
}
此时 Person 就变得很灵活了,当需要不同的咖啡机时,无需修改 Person 类的代码。
并且很容易进行测试:
kotlin
class ExampleUnitTest {
class FakeTestMachine : CoffeeMaker {
var coffeeWasMade = false
override fun makeCoffee() {
println("(Test: Pretend to be making coffee)")
coffeeWasMade = true
}
}
@Test
fun person_startMorning() {
val testMachine = FakeTestMachine()
val testPerson = Person(testMachine)
testPerson.startMorning()
assertTrue(testMachine.coffeeWasMade)
println("Test passed!")
}
}
Hilt 依赖注入框架
Hilt 是一个依赖注入框架,我想大家对于它已不再陌生。
添加依赖和启用 Hilt
首先,在项目配置文件中添加 Hilt 和 KSP 插件。
kotlin
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
...
id("com.google.dagger.hilt.android") version "2.57.1" apply false
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
注意:KSP 的版本需要和当前的 Kotlin 版本(
2.0.21)保持一致:KSP 所有版本。
然后,在应用配置文件中添加以下依赖项:
kotlin
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
dependencies {
implementation("com.google.dagger:hilt-android:2.57.1")
ksp("com.google.dagger:hilt-android-compiler:2.57.1")
}
最后,在 Application 类上添加 @HiltAndroidApp 注解,以启用 Hilt 的功能。
kotlin
@HiltAndroidApp
class MyApplication : Application()
别忘了将这个 Application 注册到清单文件中:
xml
<application
android:name=".MyApplication">
简单用法
Hilt 的注入功能(提供依赖项),支持以下 Android 类:
Application(通过@HiltAndroidApp)ViewModel(通过@HiltViewModel)ActivityFragmentViewServiceBroadcastReceiver
除了 Application 和 ViewModel 外,其他的类都是通过 @AndroidEntryPoint 注解来声明的。
以 Activity 为例,我们来简单使用一下:
kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
// 拿铁制造机由 Hilt 注入
@Inject
lateinit var latteMachine: LatteMachine
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {}
latteMachine.makeCoffee()
}
}
注意:使用
@Inject进行字段注入,被注入的字段不能是private的。而通过构造函数注入的参数,则可以是private的。
在 LatteMachine 类的无参构造函数上添加 @Inject,让 Hilt 知道拿铁机的"制造"方法。
kotlin
class LatteMachine @Inject constructor() : CoffeeMaker {
override fun makeCoffee() {
println("Making... Latte") // 🥛☕️
}
}
运行程序,结果如下:
less
Making... Latte
咖啡机确实制作了一杯拿铁。
带参的依赖注入
我们再来看看带参数的构造函数的注入,给 LatteMachine 的构造函数添加一个牛奶起泡器的参数:
kotlin
class LatteMachine @Inject constructor(
private val milkFrother: MilkFrother
) : CoffeeMaker {
override fun makeCoffee() {
milkFrother.frothMilk()
println("Making... Latte") // 🥛☕️
}
}
对于参数 milkFrother,Hilt 会自动寻找并尝试注入它。
我们只需让 Hilt 知道牛奶起泡器的"制造"方法即可,和之前一样:
kotlin
/**
* 添加 @Inject constructor,让 Hilt 知道如何创建它
*/
class MilkFrother @Inject constructor() {
fun frothMilk() {
println("(The milk is frothing...)")
}
}
运行结果:
less
(The milk is frothing...)
Making... Latte
只有 LatteMachine 构造函数所依赖的参数都能被注入,LatteMachine 才能被注入。
复杂注入
接口
以之前的 Person 类为例,我们来完成 CoffeeMaker 接口的注入。
kotlin
class Person @Inject constructor(
// 咖啡制造机由 Hilt 注入
private val coffeeMaker: CoffeeMaker
) {
fun startMorning() {
println("Good morning, I need coffee...")
coffeeMaker.makeCoffee()
println("The coffee is finished and work is done.")
}
}
因为接口没有构造函数,所以 Hilt 并不知道如何创建该接口的实例。
这时,我们可以创建一个抽象类 CoffeeMakerModule,在其中提供该实例:
kotlin
@InstallIn(ActivityComponent::class) // 暂时先安装在 ActivityComponent
@Module // 表示这是一个用来提供依赖注入实例的模块
abstract class CoffeeMakerModule {
@Binds
abstract fun bindCoffeeMaker(americanoMachine: AmericanoMachine): CoffeeMaker // 给 CoffeeMaker 接口提供实现,提供的实例为 americanoMachine 参数
}
再告诉 Hilt 美式咖啡机的"制造"方法即可。
kotlin
class AmericanoMachine @Inject constructor() : CoffeeMaker {
override fun makeCoffee() {
println("Making... Americano") // ☕️
}
}
现在 Person 中的 CoffeeMaker 接口对象就能被注入了。
kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var person: Person
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {}
person.startMorning()
}
}
运行结果:
less
Good morning, I need coffee...
Making... Americano
The coffee is finished and work is done.
如果现在需要两台咖啡机,一台美式咖啡机,一台拿铁咖啡机,该怎么办?
这时,我们可以使用 @Qualifier 注解,来给相同的类或接口注入不同的实例:
kotlin
// 定义两个注解
@Qualifier
@Retention(AnnotationRetention.BINARY) // 注解只能被编译器读取,不能被运行时反射读取
annotation class BindAmericanoMachine
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindLatteMachine
@InstallIn(ActivityComponent::class)
@Module
abstract class CoffeeMakerModule {
@Binds
@BindAmericanoMachine
abstract fun bindCoffeeMaker(americanoMachine: AmericanoMachine): CoffeeMaker
@Binds
@BindLatteMachine
abstract fun bindLatteMachine(latteMachine: LatteMachine): CoffeeMaker
}
在用到的地方,通过这两个注解来声明当前注入的实例类型:
kotlin
class Person @Inject constructor() {
@BindAmericanoMachine
@Inject
lateinit var americanoCM: CoffeeMaker
@BindLatteMachine
@Inject
lateinit var latteCM: CoffeeMaker
fun startMorning() {
println("Good morning, I need americano and latte coffee...")
americanoCM.makeCoffee()
latteCM.makeCoffee()
println("The coffee is finished and work is done.")
}
}
运行结果:
less
Good morning, I need americano and latte coffee...
Making... Americano
(The milk is frothing...)
Making... Latte
The coffee is finished and work is done.
第三方库的类
对于第三方库提供的类,我们无法修改其源码,自然不能在其构造函数上添加 @Inject 注解。
对于这种情况的注入,我们也要使用 @Module 注解。
kotlin
@Module
@InstallIn(ActivityComponent::class) // 暂时先安装在 ActivityComponent
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
}
引入依赖:
implementation("com.squareup.okhttp3:okhttp:4.12.0")
这里没有抽象函数,自然 NetworkModule 不是抽象类。
现在,我们就可以在任何地方注入这个 OkHttpClient 实例了,比如说:
kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var okHttpClient: OkHttpClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {}
val url = "https://juejin.cn/"
val request = Request.Builder()
.url(url)
.build()
val call = okHttpClient.newCall(request)
lifecycleScope.launch(Dispatchers.IO) {
val response = call.execute()
println(response.body?.string())
}
}
}
如果遇到需要提供的实例,依赖于其他实例。这和【带参的依赖注入】是一样的,只需将所依赖的实例放到函数参数中,Hilt 会自动去寻找并注入。
kotlin
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
// ...
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl("https://juejin.cn/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
fun provideApiService(retrofit: Retrofit): ApiService {
return retrofit.create(ApiService::class.java)
}
}
添加依赖:
implementation("com.squareup.retrofit2:retrofit:2.9.0")、implementation("com.squareup.retrofit2:converter-gson:2.9.0")
kotlin
interface ApiService {
@GET("/")
suspend fun getHomepage(): retrofit2.Response<ResponseBody>
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@Inject
lateinit var apiService: ApiService
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {}
lifecycleScope.launch {
try {
val response = apiService.getHomepage()
if (response.isSuccessful) {
println(response.body()?.string())
} else {
println("API request failed:${response.code()}")
}
} catch (e: Exception) {
println("Network request exceptions:${e.message}")
}
}
}
}
全局单例
我们都知道 OkHttpClient 内部管理着连接池和线程池,为了复用资源,必须将其作为单例。
对于全局单例,也是在 Module 中进行配置:
kotlin
@Module
@InstallIn(SingletonComponent::class) // 模块安装到 SingletonComponent 组件
class NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { // 为了一致性和效率,Retrofit 也应该为单例
return Retrofit.Builder()
.baseUrl("https://juejin.cn/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): ApiService { // create() 方法开销较高(它需要解析 ApiService 接口的所有注解,并动态创建一个实现该接口的对象),必须为单例
return retrofit.create(ApiService::class.java)
}
}
核心层级:组件和组件作用域
我们之前在 @Module 上使用了 @InstallIn 注解,比如 @InstallIn(ActivityComponent::class) 或 @InstallIn(SingletonComponent::class),它是用于将当前模块安装到对应组件中的。
Hilt 组件(Component)可以理解成一个容器,它持有了它能提供的实例(依赖),每个容器都有着自己的生命周期。
生命周期
Hilt 组件和作用域注解是严格对应的,两者决定了实例的生命周期:
在同一个模块中,模块所在的组件和使用的作用域注解不一致,编译会出错。
| 生成的组件 | 作用域注解 | 生命周期 |
|---|---|---|
SingletonComponent |
@Singleton |
App 整个生命周期 |
ActivityRetainedComponent |
@ActivityRetainedScoped |
跨越配置更改的 Activity 生命周期(ViewModel 依附于此) |
ActivityComponent |
@ActivityScoped |
Activity 的生命周期(Activity 销毁,它也销毁) |
FragmentComponent |
@FragmentScoped |
Fragment 的生命周期 |
ViewModelComponent |
@ViewModelScoped |
ViewModel 的生命周期 |
以 @ActivityScoped 为例,它表示 Hilt 会为每一个新的 Activity 创建一个新的实例。
为什么我们使用 @Singleton 注解来修饰 OkHttpClient,是因为这样 Hilt 才会创建唯一的 OkHttpClient 实例,并且整个应用运行期间,都能复用这个实例。
如果只是安装到了 SingletonComponent 组件,Hilt 默认每次注入时,都会创建新的实例。只有添加了作用域注解,比如 @Singleton,OkHttpClient 实例才会在 SingletonComponent 组件复用同一个实例。
层级与可见性
Hilt 的组件之间有着严格的父子关系:

一个组件可以访问其所有父组件中的依赖,反之则不行。
作用域注解也可以在注入类上方使用,比如:
kotlin
@Singleton
class Person @Inject constructor() {
@BindAmericanoMachine
@Inject
lateinit var americanoCM: CoffeeMaker
@BindLatteMachine
@Inject
lateinit var latteCM: CoffeeMaker
fun startMorning() {
println("Good morning, I need americano and latte coffee...")
americanoCM.makeCoffee()
latteCM.makeCoffee()
println("The coffee is finished and work is done.")
}
}
Person 会自动添加到 SingletonComponent 组件中,并带有 @Singleton 注解。这样,这个 Person 实例会变为全局唯一,且全局可用。
但此时运行程序,Hilt 会直接编译报错。
因为 Person 被 @Singleton 修饰,Hilt 会在 SingletonComponent 中创建并持有这个 Person 实例。它所依赖的 CoffeeMaker 安装到了 ActivityComponent 组件,当 Person 被创建时,SingletonComponent(父)是不可能获取得到 ActivityComponent(儿)中的实例的。
所以,我们应该让 CoffeeMakerModule 是 SingletonComponent 父辈,但 SingletonComponent 已经是最顶层了,所以就只能安装到 SingletonComponent:
kotlin
@InstallIn(SingletonComponent::class) // 必须为 SingletonComponent
@Module
abstract class CoffeeMakerModule {
@Binds
@Singleton // 最好设为单例
@BindAmericanoMachine
abstract fun bindCoffeeMaker(americanoMachine: AmericanoMachine): CoffeeMaker
@Binds
@Singleton
@BindLatteMachine
abstract fun bindLatteMachine(latteMachine: LatteMachine): CoffeeMaker
}
小结一下:
@InstallIn 决定了模块被安装到了哪一层,作用域注解决定了实例在该层中是否复用。
要遵循父级不能依赖子级的规则。
预置 Qualifier
对于 Context 等系统组件,它们的实例由系统创建。如果要进行依赖注入,就要使用系统提供的 Qualifier(限定符)。
例如 @ApplicationContext 注解可以提供 ApplicationContext。
kotlin
class AmericanoMachine @Inject constructor(
@ApplicationContext private val context: Context
) : CoffeeMaker {
override fun makeCoffee() {
println("Making... Americano") // ☕️
showMessage("Americano is ready!")
}
private fun showMessage(text: String) {
Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
}
}
注意:
CoffeeMakerModule被安装到了SingletonComponent中,所以只能注入@ApplicationContext,不能注入@ActivityContext。(SingletonComponent是ActivityComponent的父级)
如果需要 Activity 类型的 Context,就使用 @ActivityContext 注解。但要确保注入它的类是在 ActivityComponent 组件或其子组件。
对于 Application 和 Activity 实例的注入,我们无需使用注解,Hilt 已经预置了注入功能。
但声明这两个类型的子类型,就会编译出错,如果我们需要 MyApplication 实例,就要去手动提供:
kotlin
@Module
@InstallIn(SingletonComponent::class)
object ApplicationModule {
@Provides
@Singleton
fun provideMyApplication(app: Application): MyApplication {
return app as MyApplication
}
}
如果 Module 中只有 @Provides,没有 @Binds,可以改为 object。
ViewModel 的依赖注入
ViewModel 是一个特殊的 Jetpack 组件,它的生命周期和注入方式由 Hilt 专门管理。我们使用 @HiltViewModel 注解即可:
kotlin
@HiltViewModel
class MyViewModel @Inject constructor(/*依赖也可以在这里注入*/) : ViewModel() {
fun doSomething() {
viewModelScope.launch {
println("MyViewModel: I'm doing something...")
delay(1000)
println("MyViewModel: I'm done!")
}
}
}
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
val viewModel: MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {}
viewModel.doSomething()
}
}
ViewModel 的生命周期和 ActivityRetainedComponent 组件一样。
Compose 集成
在 Jetpack Compose 中,我们调用 hiltViewModel() Composable 函数来提供一个 ViewModel 实例。
添加依赖:
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
kotlin
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HelloWorld()
}
}
}
@Composable
fun HelloWorld() {
val viewModel: MyViewModel = hiltViewModel()
Button(
onClick = {
viewModel.doSomething()
}
) {
Text(text = "Click me")
}
}
传递运行时参数:SavedStateHandle 模式
在真实项目中,ViewModel 在创建时,常常会依赖于一个从 Activity/Fragment 中传入的参数。
这个参数无法由 Hilt 在编译期提供,但和 Navigation 结合使用时,我们可以使用 SavedStateHandle 来完成。
kotlin
@HiltViewModel
class UserDetailViewModel @Inject constructor(
private val apiService: ApiService, // 由 Hilt 注入
private val savedStateHandle: SavedStateHandle // 由 Hilt 自动提供的、包含路由参数的句柄
) : ViewModel() {
// userId 必须和 NavHost 路由中的参数名 detail/{userId} 匹配
val userId: String = savedStateHandle["userId"]!!
init {
println("UserDetailViewModel created for user: $userId")
loadUser()
}
fun loadUser() {
viewModelScope.launch {
// 使用 apiService 和 userId 加载数据
println("Loading data for $userId...")
}
}
}
KOTLIN
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyNavHost()
}
}
}
@Composable
fun MyNavHost() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "list") {
// 列表页
composable("list") {
Button(
onClick = {
navController.navigate("detail/1")
}
) {
Text("List 1")
}
}
// 详情页路由
composable("detail/{userId}") {
// 不需要从 backStackEntry 手动提取 userId
// 直接调用 hiltViewModel()
// Hilt 会自动获取当前的 NavBackStackEntry,
// 创建 SavedStateHandle,并将其注入 ViewModel。
val detailViewModel: UserDetailViewModel = hiltViewModel()
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text(text = "Detail ${detailViewModel.userId}")
}
}
}
}
添加依赖:
implementation("androidx.navigation:navigation-compose:2.9.6")
参考
参考郭霖大神的博客:Jetpack新成员,一篇文章带你玩转Hilt和依赖注入
参考 Google 官方文档:使用 Hilt 实现依赖项注入