Android Hilt 依赖注入

前言

很早之前读了郭神写的一篇讲解依赖注入的文章: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::class
  • ActivityComponent::class
  • FragmentComponent::class
  • ViewComponent::class
  • ViewWithFragmentComponent::class
  • ServiceComponent::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::class
  • ViewModelComponent::class
  • ActivityComponent::class
  • FragmentComponent::class
  • ViewComponent::class
  • ViewWithFragmentComponent::class
  • ServiceComponent::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)

但这个功能不常用,本文不再讲解。

参考文章

相关推荐
古法安卓1 小时前
Android-深入理解 Android 回调(Callback)机制
android·性能优化·android studio
古法安卓1 小时前
Android-车机 GNSS 定位数据接收问题排查
android·java·android studio
FungLeo4 小时前
Flutter 吸顶分组列表实战:语义桶分组 + 点击头平滑滚动
android·flutter
阿巴斯甜4 小时前
Android 自定义权限
android
FungLeo5 小时前
Flutter 超长 StatefulWidget 拆分术:part of + extension on State 实战
android·flutter·dart
小王C语言6 小时前
MySQL 数据类型:数值类型、字符串类型、日期和时间类型、enum 和 set、find_in_set 查询
android·数据库·mysql
我命由我123456 小时前
Android 控件 - CardView(快速实现圆角)
android·java·java-ee·kotlin·android studio·android-studio·android runtime
码云数智-园园6 小时前
Android 全方位性能优化合集
android·性能优化
hunterandroid7 小时前
[Android 从零到一] Compose 动画实战:从 AnimatedVisibility 到复杂转场与手势联动
android