Android Jetpack - Hilt

204 阅读5分钟

Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码,它通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用依赖项注入的标准方法。

什么是依赖项注入

类通常需要引用其他类,这些类称为依赖项。Android 中的依赖项注入方式主要有两种:

一是构造函数注入,将某个类的依赖项传入其构造函数。

class Child(private val parent: Parent) {
    fun grow() {
        parent.help()
    }
}

class Parent {
    fun help() {
        println("parents' help")
    }
}

fun main() {
    val parent = Parent()
    val child = Child(parent)
    child.grow()
}

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

class Child {
    lateinit var parent: Parent
    fun grow() {
        parent.help()
    }
}

class Parent {
    fun help() {
        println("parents' help")
    }
}

fun main() {
    val child = Child()
    child.parent = Parent()
    child.grow()
}

依赖

在项目级 build.gradle 里添加

plugins {
    ...
    id 'com.google.dagger.hilt.android' version '2.44' apply false
}

在 app/build.gradle 中添加

plugins {
    ...
    id 'kotlin-kapt'
    id 'com.google.dagger.hilt.android'
}
android { ... }

dependencies {
  implementation "com.google.dagger:hilt-android:2.44"
  kapt "com.google.dagger:hilt-compiler:2.44"
}

kapt {
  correctErrorTypes true
}

应用类

所有使用 Hilt 的应用都必须包含一个带有 @HiltAndroidApp 注解的 Application 类,它会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。

@HiltAndroidApp
class HiltApplication : Application() {
    ...
}

注入 Android 类

Hilt 可以为带有 @AndroidEntryPoint 注解的其他 Android 类提供依赖项,目前支持的 Android 类有 Application,ViewModel,Activity,Fragment,View,Service 和 BroadcastReceiver。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

如果使用 @AndroidEntryPoint 为某个 Android 类添加注解,则还必须为依赖于该类的 Android 类添加注解。例如,如果为某个 fragment 添加注解,则还必须为使用该 fragment 的所有 activity 添加注解。

注意:Hilt 仅支持扩展 ComponentActivity 的 activity,如 AppCompatActivity,仅支持扩展 androidx.Fragment 的 Fragment,不支持保留的 fragment。

定义 Hilt 绑定

可以通过构造函数注入来向 Hilt 提供绑定信息,在某个类的构造函数中使用 @Inject 注解,以告知 Hilt 如何提供该类的实例。

class User @Inject constructor() {
    fun print() {
        Log.i(TAG, "print user")
    }
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    //由 Hilt 注入的字段不能为私有字段
    @Inject
    lateinit var user: User

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        user.print()
    }

}

由此可见,在 MainActivity 中并没有去创建 User 的实例,只是用 @Inject 声明一下,然后就可以调用它的方法了。

在一个类的代码中,带有注解的构造函数的参数即是该类的依赖项,如下所示,Book 是 User 的一个依赖项,因此,Hilt 还必须知道如何提供 Book 的实例,User 构造函数中所依赖的所有对象都支持依赖注入了,User 才可以被依赖注入。

class User @Inject constructor(val book: Book) {
    fun print() {
        Log.i(TAG, "print user")
    }
}
class Book @Inject constructor() {}

Hilt 模块

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

比如,我们有一个 Animal 接口和它的实现类

interface Animal {
    fun walk()
    fun run()
}

class Dog @Inject constructor() : Animal {
    override fun walk() {
        Log.i(TAG, "dog walk")
    }

    override fun run() {
        Log.i(TAG, "dog run")
    }
}

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

@Module
@InstallIn(ActivityComponent::class)
abstract class AnimalModule {
    @Binds
    abstract fun bindDog(dog: Dog): Animal
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var dog: Animal

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        dog.run()
        dog.walk()
    }

}

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

interface Animal {
    fun walk()
    fun run()
}

class Dog @Inject constructor() : Animal {
    override fun walk() {
        Log.i(TAG, "dog walk")
    }

    override fun run() {
        Log.i(TAG, "dog run")
    }
}

class Cat @Inject constructor() : Animal {
    override fun walk() {
        Log.i(TAG, "cat walk")
    }

    override fun run() {
        Log.i(TAG, "cat run")
    }
}

需要定义两个注解

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindDog

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindCat

修改 AnimalModule

@Module
@InstallIn(ActivityComponent::class)
abstract class AnimalModule {
    @BindDog
    @Binds
    abstract fun bindDog(dog: Dog): Animal

    @BindCat
    @Binds
    abstract fun bindCat(cat: Cat): Animal
}

然后注入到 Activity 中即可,别忘了给字段加上定义的注解

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @BindDog
    @Inject
    lateinit var dog: Animal

    @BindCat
    @Inject
    lateinit var cat: Animal

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        dog.run()
        dog.walk()
        cat.run()
        cat.walk()
    }

}

如果某个类不归你所有,它来自第三方,这样也无法通过构造函数注入,此时,我们可以在 Hilt 模块内创建一个函数,并使用 @Provides 为该函数添加注解,以此来告知 Hilt 如何提供此类型的实例。

这里以 OkHttp 为例,我们在使用它时,需要创建一个 OkHttpClient 实例,新建 OKHttpModule 类,给 OkHttpClient 类型提供实例。

@Module
@InstallIn(ActivityComponent::class)
class OKHttpModule {
    @Provides
    fun getOkHttpClient() = OkHttpClient().newBuilder()
        .connectTimeout(30, TimeUnit.SECONDS).build()
}

然后在 Activity 中注入

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

}

Hilt 还提供了一些预定义的限定符,例如,我们可能需要来自应用或 Activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。

class User @Inject constructor(@ActivityContext private val context: Context) {

}

为 Android 类生成的组件

对于可以从中执行字段注入的每个 Android 类,都有一个关联的 Hilt 组件,可以在 @InstallIn 注解中引用该组件,每个 Hilt 组件负责将其绑定注入相应的 Android 类,Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。

NG_PKU2T4EF3O1XY}SVEPMV.png

Hilt 不会为广播接收器生成组件,因为 Hilt 直接从 SingletonComponent 注入广播接收器。

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

I7I0L(`8F9CP3T4K_@SBZ5G.png

比如,我们需要将 User 的作用域限定为 ActivityComponent,Hilt 就会在相应 Activity 的整个生命周期内提供同一实例。

@ActivityScoped
class User @Inject constructor() {
    fun print() {
        Log.i(TAG, "print user")
    }
}