什么是依赖注入
Hilt
是 Google 官方为开发者提供的可以简化使用的依赖注入框架。在介绍它之前,我们先来看看什么是依赖注入。代码如下所示:
// 依赖注入
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
// 非依赖注入
class Car() {
private val engine = Engine()
fun start() {
engine.start()
}
}
可以看到,依赖注入其实非常简单,简单来说就是 Car
类依赖于外部传入的 Engine
类,而不是自己构造。在 Android 中有两种方式实现依赖注入,分别是,构造函数注入,将某个类的依赖项传入其构造函数;另一个是 字段注入(或 setter 注入) ,某些 Android 框架类(如 activity 和 fragment)由系统实例化,因此无法进行构造函数注入,因此使用字段注入,其依赖项将在创建类后实例化。
Hilt 与 Dagger 的关系
Dagger
是 Square 公司开发的一个依赖注入框架。由于 Dagger
最开始是采用反射的方式实现的,会影响程序的运行效率。因此 Google 基于 Dagger
开发了 Dagger2
,Dagger2
是通过注解的方式实现的。
但是 Dagger2
的使用比较繁琐,因此 Google 推出了 Hilt组件
。它是基于 Dagger
开发的,为依赖注入提供了更简便的实现方式。
Hilt 的使用
依赖配置
首先我们需要在项目根目录下的 build.gradle 添加如下插件:
plugins {
...
id("com.google.dagger.hilt.android") version "2.44" apply false
}
然后我们在 app 下的 build.gradle 就可以依赖插件和添加对应的 Hilt
依赖了。
plugins {
id("kotlin-kapt")
id("com.google.dagger.hilt.android")
}
android {
...
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
}
dependencies {
implementation("com.google.dagger:hilt-android:2.44")
kapt("com.google.dagger:hilt-android-compiler:2.44")
}
kapt {
correctErrorTypes = true
}
添加依赖项容器
在使用 Hilt
时,开发者必须自定义一个 Application
,并为其添加 @HiltAndroidApp
注解。这里创建一个 HiltApplication
类,然后将它注册到配置文件中。代码示例如下:
@HiltAndroidApp
class HiltApplication : Application() {
}
然后,我们需要在使用这些依赖注入的 Android 类中添加依赖项容器。目前支持的 Android 类有:
Application
通过使用@HiltAndroidApp
声明ViewModel
通过使用@HiltViewModel
声明Activity
仅支持ComponentActivity
及其子类,使用@AndroidEntryPoint
声明Fragment
仅支持 Androidx下的 Fragment,使用@AndroidEntryPoint
声明View
使用@AndroidEntryPoint
声明Service
使用@AndroidEntryPoint
声明BroadcastReceiver
使用@AndroidEntryPoint
声明
代码示例如下:
// 如果没有 @AndroidEntryPoint 注解,那么 engine 将不会被依赖注入
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var engine: Engine
}
依赖注入普通类
还是以上面的 Car
和 Engine
两个类为例,看看如何使用 Hilt
来实现依赖注入。代码示例如下:
// Hilt 通过为被依赖类的构造函数添加 @Inject 注解,这里是 Engine 类
// 来告知 Hilt 应如何提供该类的实例
class Engine @Inject constructor() {
fun start() {
...
}
}
class Car {
// 使用的时候也添加 @Inject 注解
@Inject
lateinit var engine: Engine
fun start() {
engine.start()
}
}
注意:由 Hilt 注入的字段不能为私有字段。尝试使用 Hilt 注入私有字段会导致编译错误。
如果 Engine
也需要依赖参数怎么办?假设 Engine
需要一个 Config
来配置自己,这时候我们只需要给 Config
也加上 @Inject
注解就可以了。代码如下:
class Config @Inject constructor() {
}
还有一点需要注意,如果你用 new
的方式创建了 Car
对象,内部的 engine
是不会被初始化的。解决方法是让 Car
也增加 @Inject
注解,使用 Hilt
的方式来获取。
// 这样是不行的,会crash
val car = Car()
car.engine.start()
依赖注入接口
如下代码所示,有一个分析服务的接口 AnalyticsService
以及它的实现类 AnalyticsServiceImpl
。
// 定义一个分析服务的接口
interface AnalyticsService {
fun analyticsMethods()
}
// 具体的实现类
class AnalyticsServiceImpl @Inject constructor() : AnalyticsService {
override fun analyticsMethods() {
...
}
}
我们希望可以使用 AnalyticsServiceImpl
来依赖注入这个接口。在 Hilt
中,我们可以通过 @Binds
注解来实现。代码示例如下:
// @Module 作用是告知 Hilt 如何提供某些类型的实例
// @InstallIn 告知 Hilt 每个模块将用在哪个 Android 类中
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
// @Binds 标识提供的接口实现的方法,其中方法参数表示提供哪种实现;
// 方法返回值表示该函数提供哪个接口的实例
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
// 使用方法还是一样的
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var service: AnalyticsService
}
多个接口实现
继续上面的示例,如果有多个 AnalyticsService
接口的实现类,我们需要在不同的场景下使用不同的实现类。这时候我们就需要使用 Qualifier
注解。
假设目前我们有两种分析服务的实现,一种是分析Java代码的,一种是分析Jni代码的:
// java代码分析服务
class JavaAnalyticsServiceImpl @Inject constructor() : AnalyticsService {
...
}
// jni分析服务
class JniAnalyticsServiceImpl @Inject constructor() : AnalyticsService {
...
}
我们分别为其提供 Hilt
依赖注入所需要的抽象类
@InstallIn(ApplicationComponent::class)
@Module
abstract class JavaAnalyticsModule {
@Singleton
@Binds
abstract fun bindJavaAnalytics(impl: JavaAnalyticsServiceImpl): AnalyticsService
}
@InstallIn(ActivityComponent::class)
@Module
abstract class JniAnalyticsModule {
@ActivityScoped
@Binds
abstract fun bindJniAnalytics(impl: JniAnalyticsServiceImpl): AnalyticsService
}
然后我们需要为不同的 @Binds
方法定义并添加注解的限定符,代码如下:
// 定义限定符
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class JavaAnalytics
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class JniAnalytics
// 在我们创建的`@Binds` 方法上添加对应的限定符
@InstallIn(ApplicationComponent::class)
@Module
abstract class JavaAnalyticsModule {
@JavaAnalytics // 添加对应的限定符
@Singleton
@Binds
abstract fun bindJavaAnalytics(impl: JavaAnalyticsServiceImpl): AnalyticsService
}
@InstallIn(ActivityComponent::class)
@Module
abstract class JniAnalyticsModule {
@JniAnalytics // 添加对应的限定符
@ActivityScoped
@Binds
abstract fun bindJniAnalytics(impl: JniAnalyticsServiceImpl): AnalyticsService
}
最后在使用的地方也添加限定符就可以了,代码如下:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@JniAnalytics
@Inject
lateinit var service: AnalyticsService
}
依赖注入三方库
一般来说,第三方库我们是无法修改的,因此无法手动添加注解。如果我们需要让第三方库支持依赖注入,这时就可以使用 @Provides
注解。代码示例如下:
@Module
@InstallIn(ActivityComponent::class)
class NetWorkHelper {
@Provides
fun getOkHttpClient(): OkHttpClient {
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS)
.build()
return okHttpClient
}
}
预定义限定符
除了自定义依赖注入外,Hilt
提供了一些预定义的限定符,如 @ApplicationContext
和 @ActivityContext
,方便开发使用。代码示例如下:
class UserManager @Inject constructor(
@ActivityContext private val context: Context
) {
...
}
生命周期
前面提到过 @InstallIn
注解的主要作用是告知 Hilt
每个模块将用在哪个 Android 类中。有了这个信息,就方便管理依赖注入生成的实例的生命周期了。除了上面介绍的 ActivityComponent
模块外,所有组件的对应的生命周期如下表所示:
组件 | 创建时机 | 销毁时机 |
---|---|---|
SingletonComponent | Application#onCreate() | Application 已销毁 |
ActivityRetainedComponent | Activity#onCreate() | Activity#onDestroy() |
ViewModelComponent | ViewModel 已创建 | ViewModel 已销毁 |
ActivityComponent | Activity#onCreate() | Activity#onDestroy() |
FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() |
ViewComponent | View#super() | View 已销毁 |
ViewWithFragmentComponent | View#super() | View 已销毁 |
ServiceComponent | Service#onCreate() | Service#onDestroy() |
依赖注入的对象作用域
默认情况下,每一次声明依赖绑定时,Hilt
都会创建所需类型的一个新实例。如下代码所示,会创建三个不同的对象。
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var analyticsService1: AnalyticsService
@Inject
lateinit var analyticsService2: AnalyticsService
@Inject
lateinit var analyticsService3: AnalyticsService
...
}
如果你只想在当前 Activity
只有有一个 AnalyticsService
对象,这时就可以使用 @ActivityScoped
作用域。Hilt
只会为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。代码示例如下:
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {
@ActivityScoped // 设置 ActivityScoped 作用域
@Binds
abstract fun bindAnalyticsService(
analyticsServiceImpl: AnalyticsServiceImpl
): AnalyticsService
}
这时在当前 Activity
就只会有一个 AnalyticsService
对象。除了 @ActivityScoped
之外,Hilt
还提供了其他的作用域。Hilt
所有的作用域如下表所示:
作用域 | 范围 |
---|---|
@Singleton | Application ,全局单例 |
@ActivityRetainedScoped | Activity 范围内 |
@ViewModelScoped | ViewModel 范围内 |
@ActivityScoped | Activity 范围内 |
@FragmentScoped | Fragment 范围内 |
@ViewScoped | View 范围内 |
@ViewScoped | 带有 @WithFragmentBindings 注解的 View |
@ServiceScoped | Service 范围内 |
需要注意的是,作用域需要与 Hilt
组件对应。比如说 @ActivityScoped
作用域,需要 @InstallIn(ActivityComponent::class)
才行。组件与作用域之间的对应关系如下表所示:
组件 | 作用域 |
---|---|
SingletonComponent | @Singleton |
ActivityRetainedComponent | @ActivityRetainedScoped |
ViewModelComponent | @ViewModelScoped |
ActivityComponent | @ActivityScoped |
FragmentComponent | @FragmentScoped |
ViewComponent | @ViewScoped |
ViewWithFragmentComponent | @ViewScoped |
ServiceComponent | @ServiceScoped |