Hilt组件解析

语言: CN / TW / HK

、为什么使用Hilt

Hilt是Android的依赖注入库。回答为什么要使用Hilt这个问题其实就是回答:什么是依赖注入库?为什么要使用依赖注入库?Hilt解决了什么问题?

什么是依赖注入?

依赖注入即Dependency Injection,简称DIHiltDaggerKoin等都是依赖注入库。依赖注入最主要是帮助代码解耦和测试。

类通常需要引用其它类,比如Car类可能需要引用Engine类,这些必需类称为依赖项。

类获取依赖项的方式有三种:

  1. 类构造其所需的依赖项
  2. 从其他地方抓取,例如 Context gettergetSystemService()
  3. 以参数的形式提供,这种方式也称依赖注入,基于控制反转原则。在构造类时提供这些依赖项,或者将这些依赖项传入需要各个依赖项的函数。

```kotlin class Car {

private val engine = Engine()

fun start() {
    engine.start()
}

} ```

上述示例在Car类中构造了Engine对象实例,这可能会有问题:

  1. CarEngine关系过于紧密。Car 的实例使用一种类型的 Engine,并且无法轻松使用子类或替代实现。如果 Car 要构造自己的 Engine,您必须创建两种类型的 Car,而不是直接将同一 Car 重用于 GasElectric 类型的引擎。
  2. Engine 的强依赖使得测试更加困难。

如果使用依赖注入

```kotlin class Car(private val engine: Engine) { fun start() { engine.start() } }

fun main(args: Array) { val engine = Engine() val car = Car(engine) car.start() } ```

Car 依赖于 Engine,因此应用会创建 Engine 的实例,然后使用它构造 Car 的实例。

  1. 重用Car。可以将 Engine 的不同实现传入 Car
  2. 轻松测试Car。您可以传入测试替身以测试不同场景。

依赖注入库的作用

Android 中有两种主要的依赖项注入方式:

  1. 构造函数注入

如前述示例所示,将某个类的依赖项传入其构造函数

  1. 字段注入

某些 Android 框架类(如 ActivityFragment)由系统实例化,因此无法进行构造函数注入。使用字段注入时,依赖项将在创建类后实例化。

```kotlin class Car { lateinit var engine: Engine

   fun start() {
       engine.start()
   }

}

fun main(args: Array) { val car = Car() car.engine = Engine() car.start() } ```

我们可以自行创建、提供并管理不同类的依赖项,即手动依赖注入。随着依赖项和类的增多,这种方式会特别繁琐,带来一些问题

  1. 对于大型应用,获取所有依赖项并正确连接它们可能需要大量样板代码
  2. 如果您无法在传入依赖项之前构造依赖项,则需要编写并维护管理内存中依赖项生命周期的自定义容器

Dagger是适用于 JavaKotlinAndroid 的热门依赖项注入库,由 Google 进行维护。Dagger 为您创建和管理依赖关系图,从而便于您在应用中使用 DI。它提供了完全静态和编译时依赖项,解决了基于反射的解决方案(如 Guice)的诸多开发和性能问题。

为什么使用Hilt依赖注入库

Hilt 在热门 DIDagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 Android Studio的支持。Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。

简单来说,就是门槛较低且好用,还有官方加持。

二、基本使用

使用Hilt依赖注入库,首要引入组件相关依赖。

Hilt的使用依赖引入与配置参见官方文档,这里不再赘述

2.1 Hilt组件

介绍Hilt常用注解的使用前,先看一下Hilt的组件。

Hilt组件层级

Hilt提供了一组内置的组件,自动集成到Android应用程序的各种生命周期中。

组件上方的注解是作用域注解,作用域绑定到该组件的生命周期。子组件的绑定可以依赖于父类组件中的任何绑定。

@InstallIn模块中定义绑定时,绑定的范围必须与组件的范围匹配。例如,@InstallIn(ActivityComponent.class)模块内的绑定只能使用@ActivityScoped.

Hilt组件注入

当使用Hilt@AndroidEntryPoint注入自己的Android类时,Hilt组件被作为注入器,确定哪些绑定对该Android类可见

| 组件 | 注入器面向的对象 | | :-------------------------: | :-------------------------------: | | SingletonComponent | Application | | ViewModelComponent | ViewModel | | ActivityComponent | Activity | | FragmentComponent | Fragment | | ViewComponent | View | | ViewWithFragmentComponent | View with @WithFragmentBindings | | ServiceComponent | Service |

注意Hilt 没有为 broadcast receivers 提供组件,因为 Hilt 直接从 SignletonComponent 注入 broadcast receivers

Hilt组件生命周期

组件的生命周期通常受限于 Android 类的相应实例的创建和销毁

| 组件 | 范围 | 创建于 | 销毁于 | | :-----------------------------: | :-----------------------: | :----------------------------------------------------------: | :----------------------------------------------------------: | | SingletonComponent | @Singleton | Application#onCreate() | Application#onDestroy() | | ActivityRetainedComponent | @ActivityRetainedScoped | Activity#onCreate()1 | Activity#onDestroy()1 | | ViewModelComponent | @ViewModelScoped | ViewModel创建 | ViewModel毁坏 | | ActivityComponent | @ActivityScoped | Activity#onCreate() | Activity#onDestroy() | | FragmentComponent | @FragmentScoped | Fragment#onAttach() | Fragment#onDestroy() | | ViewComponent | @ViewScoped | View#super() | View毁坏 | | ViewWithFragmentComponent | @ViewScoped | View#super() | View毁坏 | | ServiceComponent | @ServiceScoped | Service#onCreate() | Service#onDestroy() |

默认情况,Hilt 中的所有绑定都未限定作用,即每当应用请求绑定时,Hilt都会创建所需类型的一个新实例。Hilt允许将绑定的作用域限定为特定组件,Hilt 只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。

kotlin @ActivityScoped class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... }

使用 @ActivityScopedAnalyticsAdapter 的作用域限定为 ActivityComponentHilt 会在相应 Activity 的整个生命周期内提供 AnalyticsAdapter 的同一实例。

注意

将绑定的作用域限定为某个组件的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定

如果绑定的内部状态要求在某一作用域内使用同一实例,或者绑定的创建成本很高,那么将绑定的作用域限定为某个组件是一种恰当的做法。

每个 Hilt 组件都带有一组默认绑定,可以作为依赖项注入到您自己的自定义绑定中。

| 组件 | 默认绑定 | | :-----------------------------: | :------------------------------------------: | | SingletonComponent | Application | | ActivityRetainedComponent | Application | | ViewModelComponent | SavedStateHandle | | ActivityComponent | Application,Activity | | FragmentComponent | Application, Activity,Fragment | | ViewComponent | Application, Activity,View | | ViewWithFragmentComponent | Application, Activity, Fragment,View | | ServiceComponent | Application,Service |

ActivityRetainedComponent存在于配置更改中,因此它是在第一个 onCreate 和最后一个 onDestroy 时创建的

2.2 应用与入口点

Hilt 常用注解包含 @HiltAndroidApp@AndroidEntryPoint@Inject@Module@InstallIn@Provides@EntryPoint 等等。

应用@HiltAndroidApp

每个Android程序中,都会有一个Application,可以自定义或系统默认。在Hilt,必须自定义一个Application,否则Hilt将无法正常工作。

kotlin @HiltAndroidApp class MyApplication : Application() { }

@HiltAndroidApp 注解将会触发 Hilt 代码的生成,作为应用程序依赖项容器的基类,其生成的Hilt组件依附于Application的生命周期

入口点 @AndroidEntryPoint

Application 中设置好 @HiltAndroidApp 之后,可以使用@AndroidEntryPointAndroid类中启用成员注入。@AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项。

可使用@AndroidEntryPoint的类型为:

  1. Activity
  2. Fragment
  3. View
  4. Service
  5. BroadcastReceiver

ViewModel通过独立的Api @HiltViewModel提供支持

使用@AndroidEntryPoint 为某个Android类添加注解时,必须为依赖于该类的Android类也添加注解。例如为Fragment添加了注解,必须为使用该Fragment的所有Activity添加注解,否则会抛异常

java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity

看一下使用示例

```kotlin class Truck @Inject constructor(){

fun deliver() {
    println("deliver banner 者文公众号:者文静斋")
}

} ```

```kotlin @AndroidEntryPoint class HiltFirstActivity:AppCompatActivity() { private lateinit var mBinding:ActivityHiltFirstBinding

@Inject
lateinit var mTruck: Truck

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    mBinding = ActivityHiltFirstBinding.inflate(layoutInflater)
    setContentView(mBinding.root)
    mTruck.deliver()
}

} ```

其中Activity仅支持ComponentActivity的子类,例如FragmentActivityAppCompatActivity

Fragment仅支持继承 androidx.FragmentFragment

实例注入@Inject

@Inject用于告知Hilt如何提供该类的实例,常用于构造函数,非私有字段,方法中。尤其注意需要注入的字段不能声明为private

```kotlin class Truck @Inject constructor(){

fun deliver() {
    println("deliver banner 者文公众号:者文静斋")
}

} ```

在类的构造函数中使用@Inject注解,告知Hilt如何提供该类实例。

如果构造函数有参数

```kotlin class Truck @Inject constructor(val driver: Driver) {

fun deliver() {
    println("Truck is delivering cargo. Driven by $driver")
}

} ```

kotlin class Driver @Inject constructor()

一个类中,带有注解的构造函数的参数即为该类的依赖项。

前述DriverTruck的一个依赖项,则Hilt需要指导如何提供Driver的实例。只有Truck的构造函数中所依赖的所有其他对象都支持依赖注入,则Truck才可以被依赖注入

2.3 Hilt模块

有时,类型不能通过构造函数注入。例如,您不能通过构造函数注入接口,无法通过构造函数注入不归你所有的类型,例如外部库的类。在这些情况下,可以使用 Hilt 模块向 Hilt 提供绑定信息。

Hilt模块是一个带有@Module注解的类,它会告知Hilt如何提供某些类型的实例。必须使用@InstallIn注解为Hilt模块添加注释,告知Hilt每个模块将用在或安装在哪个Android类中。

2.3.1 @Binds注入接口实例

接口没有构造函数,无法通过构造函数注入,则应向Hilt提供绑定信息,即在Hilt模块内创建一个带@Binds注解的抽象函数,告知Hilt在需要提供接口实例的时候使用哪种实现。

@Binds注解的函数应向Hilt提供如下信息:

  1. 函数返回类型:告知 Hilt 函数提供哪个接口的实例
  2. 函数参数:告知 Hilt 要提供哪种实现

看一下代码示例

kotlin interface Engine { fun start() fun shutdown() }

先通过依赖注入的方式实现该接口的实现类

```kotlin class GasEngine @Inject constructor() : Engine { ... }

class ElectricEngine @Inject constructor() : Engine { ... } ```

然后,新建一个抽象类,在这个模块中提供Engine接口需要的实例

kotlin @Module @InstallIn(ActivityComponent::class) abstract class EngineModule { @Binds abstract fun bindEngine(gasEngine: GasEngine): Engine }

  • EngineModule 的上方声明一个 @Module 注解,表示这一个用于提供依赖注入实例的模块
  • @InstallIn 注解,用于确定 将模块安装到哪个Hilt 组件中。
  • 定义为抽象函数是因为不需要具体的函数体。函数名无所谓
  • 示例中抽象函数的返回值必须是Engine,表示用于给Engine类型接口提供实例;提供的实例由抽象函数的接收参数决定

最后,在Truck类中注入实例

```kotlin class Truck @Inject constructor(val driver: Driver) {

@Inject
lateinit var engine: Engine

fun deliver() {
    engine.start()
    println("Truck is delivering cargo. Driven by $driver")
    engine.shutdown()
}

} ```

@Module

@Module常用于创建依赖类的对象(例如第三方库 OkHttpRetrofit等等)和接口实例注入。使用 @Module 注解的类,需要使用 @InstallIn 注解指定 module 的范围。

@InstallIn

使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围。例如使用 @InstallIn(ActivityComponent::class) 注解的 module 会绑定到 activity 的生命周期上,也意味着该Module中所有依赖项都可以在应用的所有Activity中使用。

@Binds

@Binds注解告诉 Hilt 需要提供接口实例时使用哪个实现,需要在方法参数里面明确指明接口的实现类。

2.3.2 @Provides注入第三方类实例

如果某个类不归你所有或者必须使用构建器模式创建实例(例如RetrofitOkHttpRoom等),则无法通过构造函数注入,则可以主动告知Hilt如何提供此类型的实例,即在Hilt模块内创建一个函数,并使用@Provides注解为该函数添加注释。

@Provides注解的函数可向Hilt提供以下信息:

  1. 函数返回类型:告知 Hilt 函数提供哪个类型的实例
  2. 函数参数:告知 Hilt 相应类型的依赖项
  3. 函数主体:告知 Hilt 如何提供相应类型的实例。需要提供该类型实例时,Hilt会执行该函数主体

OKHttpClinet为例

```kotlin @Module @InstallIn(ActivityComponent::class) class NetworkModule {

@Provides
fun provideOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder()
    .connectTimeout(20, TimeUnit.SECONDS)
    .readTimeout(20, TimeUnit.SECONDS)
    .writeTimeout(20, TimeUnit.SECONDS)
    .build()
}

} ```

  • 函数名无所谓,示例中返回值必须为OkHttpClient,因为要提供OkHttpClient类型实例

  • 函数体中,按照常规写法创建OkHttpClient实例即可

```kotlin @AndroidEntryPoint class MainActivity : AppCompatActivity() {

@Inject
lateinit var okHttpClient: OkHttpClient
...

} ```

如果希望在 NetworkModule 中给 Retrofit 类型提供实例,而在创建 Retrofit 实例的时候,我们又可以选择让其依赖 OkHttpClient

```kotlin @Module @InstallIn(ActivityComponent::class) class NetworkModule {

...

@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder()
    .addConverterFactory(GsonConverterFactory.create())
    .baseUrl("https://zhewendev.github.io/")
    .client(okHttpClient)
    .build()
}

} ```

```kotlin @AndroidEntryPoint class MainActivity : AppCompatActivity() {

@Inject
lateinit var retrofit: Retrofit
...

} ```

Hilt组件部分已解释,默认情况,Hilt 中的所有绑定都未限定作用,即每当应用请求绑定时,Hilt都会创建所需类型的一个新实例。RetrofitOkHttpClient 的实例理论上全局只需要一份,可以借助@Singleton注解更改这一行为

```kotlin @Module @InstallIn(SingletonComponent::class) class NetworkModule {

@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
    return OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .writeTimeout(20, TimeUnit.SECONDS)
        .build()
}

@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
    return Retrofit.Builder()
        .addConverterFactory(GsonConverterFactory.create())
        .baseUrl("https://zhewendev.github.io/")
        .client(okHttpClient)
        .build()
}

} ```

2.3.3 为同一类型提供多个绑定

如果您需要让 Hilt 以依赖项的形式提供同一类型的不同实现,必须向 Hilt 提供多个绑定。

前述的Engine接口实现类有两个,通过EngineModule中的bindEngine()函数为Engine接口提供实例,这个实例要么是GasEngine,要么是ElectricEngine,如何实现同时为一个接口提供两中不同的实例。解决这一问题就需要限定符。

@Qualifier限定符是一种注释,当为某个类型定义了多个绑定时,您可以使用它来标识该类型的特定绑定。

@Qualifier注解用于给相同类型的类或接口注入不同的实例

看一下代码示例

先定义要用于为@Binds@Provides方法添加注释的限定符

```kotlin @Qualifier @Retention(AnnotationRetention.BINARY) annotation class BindGasEngine

@Qualifier @Retention(AnnotationRetention.BINARY) annotation class BindElectricEngine ```

  • @Retention:用于声明注解的作用范围。

选择 AnnotationRetention.BINARY 表示该注解在编译之后会得到保留,但是无法通过反射去访问这个注解。

然后,Hilt需要知道如何提供与每个限定符对应的类型的实例

```kotlin @Module @InstallIn(ActivityComponent::class) abstract class EngineModule {

@BindGasEngine
@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine

@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

} ```

最后,获取相应的类型实例

```kotlin class Truck @Inject constructor(val driver: Driver) {

@BindGasEngine
@Inject
lateinit var gasEngine: Engine

@BindElectricEngine
@Inject
lateinit var electricEngine: Engine

fun deliver() {
    gasEngine.start()
    electricEngine.start()
    println("Truck is delivering cargo. Driven by $driver")
    gasEngine.shutdown()
    electricEngine.shutdown()
}

} ```

示例定义了gasEngineelectricEngine两个字段,类型都是 Engine。但是在 gasEngine 的上方,使用了 @BindGasEngine 注解,这样 Hilt 就会给它注入 GasEngine 的实例;electricEngine同理。

2.3.4 预定义限定符

Android 开发中很多地方需要依赖于 Context,如果我们想要依赖注入的类,它又是依赖于 Context 的,这个情况要如何解决呢?

kotlin @Singleton class Driver @Inject constructor(val context: Context) { }

这里编译项目会报错,因为不知道如何提供context这个参数。

Android提供了一些预定义Qualifier,专门用于给我们提供Context类型的依赖注入实例

kotlin @Singleton class Driver @Inject constructor(@ApplicationContext val context: Context) { }

这种写法,Hilt会自动提供一个Application类型的Context给到Truck类当中

如果需要Activity类型ContextHilt 还预置了另外一种 Qualifier,我们使用 @ActivityContext 即可

kotlin @Singleton class Driver @Inject constructor(@ActivityContext val context: Context) { }

这里编译会报错,DriverSingleton 的,也就是全局都可以使用,但是却依赖了一个 Activity 类型的 Context,这很明显是不可能的。将其注解改为@ActivityScoped@FragmentScoped@ViewScoped,或者直接删掉都可以,这样再次编译就不会报错了。

对于 ApplicationActivity 这两个类型,Hilt 也是给它们预置好了注入功能。也就是说,如果你的某个类依赖于 Application 或者 Activity,不需要想办法为这两个类提供依赖注入的实例,Hilt 自动就能识别它们

```kotlin class Driver @Inject constructor(val application: Application) { }

class Driver @Inject constructor(val activity: Activity) { } ```

注意必须是 ApplicationActivity 这两个类型,即使是声明它们的子类型,编译都无法通过。

如果在自定义的MyApplication中提供一些全局通用函数,导致很多地方都要依赖于写的MyApplication,而MyApplication又不被HIlt识别,这时候可以做个向下类型转换就可以

```kotlin @Module @InstallIn(SingletonComponent::class) class ApplicationModule {

@Provides
fun provideMyApplication(application: Application): MyApplication {
    return application as MyApplication
}

} ```

接下来,你在Truck类中可以这样声明依赖

kotlin class Driver @Inject constructor(val application: MyApplication) { }

2.4 ViewModel依赖注入

普通方式

对于ViewModel,可以使用Hilt普通的方式来实现实例注入

比如有一个Repository类表示仓库层

kotlin class Repository @Inject constructor() { ... }

然后有一个MyViewModel继承自ViewModel,用于表示ViewModel

kotlin @ActivityRetainedScoped class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() { ... }

然后在MainActivity中通过依赖注入的方式得到MyViewModel实例

```kotlin @AndroidEntryPoint class MainActivity : AppCompatActivity() {

@Inject
lateinit var viewModel: MyViewModel
...

} ```

这种用法field不能是 private 类型;同时要加上 lateinit,即稍后初始化

通过这种方式注入ViewModel,只是一个普通对象,在Activity销毁时也会被回收,无法做到资源配置发生变更时依旧保存

独立依赖注入

对于 ViewModel 这种常用 Jetpack 组件,Hilt 专门为其提供了一种独立的依赖注入方式

ViewModel实例对象的注入需要使用@HiltViewModel注解

kotlin @HiltViewModel class FooViewModel @Inject constructor( val handle: SavedStateHandle, val foo: Foo ) : ViewModel

kotlin @AndroidEntryPoint class MyActivity : AppCompatActivity() { private val fooViewModel: FooViewModel by viewModels() }

2.5 不支持类中注入依赖

Hilt 支持最常见的 Android 类。不过有时可能需要在 Hilt 不支持的类中执行字段注入。

Hilt一共支持6个入口点,Hilt支持的入口少了一个关键的Android组件:ContentProvider,主要原因是 ContentProvider 的生命周期问题。

ContentProvider 的生命周期比较特殊,它在 ApplicationonCreate() 方法之前就能得到执行,而 Hilt 的工作原理是从 ApplicationonCreate() 方法中开始的,也就是说在这个方法执行之前,Hilt 的所有功能都还无法正常工作。

在这些情况下,可以使用 @EntryPoint 注释创建入口点。

首先,可以在ContentProvider中自定义一个自己的入口点,然后在其中定义好要依赖注入的类型

```kotlin class MyContentProvider : ContentProvider() {

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    fun getRetrofit(): Retrofit
}
...

} ```

MyEntryPoint 中定义了一个 getRetrofit() 函数,并且函数的返回类型就是 Retrofit。而Retrofit是我们已支持依赖注入的类型。

如果我们想要在 MyContentProvider 的某个函数中获取 Retrofit 的实例,只需

```kotlin class MyContentProvider : ContentProvider() {

...
override fun query(...): Cursor {
    context?.let {
        val appContext = it.applicationContext
        val entryPoint = EntryPointAccessors.fromApplication(appContext, MyEntryPoint::class.java)
        val retrofit = entryPoint.getRetrofit()
    }
    ...
}

} ```

借助 EntryPointAccessors 类,我们调用其 fromApplication() 函数来获得自定义入口点的实例,然后再调用入口点中定义的 getRetrofit() 函数就能得到 Retrofit 的实例了

三、Hilt工作原理

关于Hilt的工作原理,可以参看Hilt 工作原理 | MAD Skills,这里不再展开和赘述