一文理解Jetpack——Hilt

1,151 阅读7分钟

屏幕截图 2024-05-02 102507.png

什么是依赖注入

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开发了 Dagger2Dagger2 是通过注解的方式实现的。

但是 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

}

依赖注入普通类

还是以上面的 CarEngine 两个类为例,看看如何使用 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 模块外,所有组件的对应的生命周期如下表所示:

组件创建时机销毁时机
SingletonComponentApplication#onCreate()Application 已销毁
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ViewModelComponentViewModel 已创建ViewModel 已销毁
ActivityComponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()View 已销毁
ViewWithFragmentComponentView#super()View 已销毁
ServiceComponentService#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 所有的作用域如下表所示:

作用域范围
@SingletonApplication,全局单例
@ActivityRetainedScopedActivity 范围内
@ViewModelScopedViewModel 范围内
@ActivityScopedActivity 范围内
@FragmentScopedFragment 范围内
@ViewScopedView 范围内
@ViewScoped带有 @WithFragmentBindings 注解的 View
@ServiceScopedService 范围内

需要注意的是,作用域需要与 Hilt 组件对应。比如说 @ActivityScoped 作用域,需要 @InstallIn(ActivityComponent::class) 才行。组件与作用域之间的对应关系如下表所示:

组件作用域
SingletonComponent@Singleton
ActivityRetainedComponent@ActivityRetainedScoped
ViewModelComponent@ViewModelScoped
ActivityComponent@ActivityScoped
FragmentComponent@FragmentScoped
ViewComponent@ViewScoped
ViewWithFragmentComponent@ViewScoped
ServiceComponent@ServiceScoped

参考