Hilt 入门指南:从 DI 原理到核心用法

为什么需要依赖注入?

依赖注入(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 个类。

并且无法对 PersonstartMorning() 进行单元测试。

在创建 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
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

除了 ApplicationViewModel 外,其他的类都是通过 @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 默认每次注入时,都会创建新的实例。只有添加了作用域注解,比如 @SingletonOkHttpClient 实例才会在 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(儿)中的实例的。

所以,我们应该让 CoffeeMakerModuleSingletonComponent 父辈,但 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。(SingletonComponentActivityComponent 的父级)

如果需要 Activity 类型的 Context,就使用 @ActivityContext 注解。但要确保注入它的类是在 ActivityComponent 组件或其子组件。

对于 ApplicationActivity 实例的注入,我们无需使用注解,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 实现依赖项注入

相关推荐
曹绍华2 小时前
android 线程loop
android·java·开发语言
介一安全2 小时前
【Frida Android】实战篇3:基于 OkHttp 库的 Hook 抓包
android·okhttp·网络安全·frida
sTone873752 小时前
Android Room部件协同使用
android·前端
我命由我123452 小时前
Android 开发 - Android JNI 开发关键要点
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
千码君20162 小时前
Android Emulator hypervisor driver is not installed on this machine
android
lichong9512 小时前
Android studio release 包打包配置 build.gradle
android·前端·ide·flutter·android studio·大前端·大前端++
傲世(C/C++,Linux)2 小时前
Linux系统编程——进程通信之有名管道
android·linux·运维
Hy行者勇哥4 小时前
物联网工控一体机操作系统选型:安卓、Ubuntu、Debian 场景化决策指南
android·物联网·ubuntu