阅读 429

Hilt入门 看这一篇就够了!!

关于依赖注入,很多小伙伴都没怎么使用过,觉得没有什么用,而且使用起来很麻烦,不过一旦学习完了,你会发现对平时开发有很大的作用。

相比于之前的Dagger,Android中建议使用Hilt来完成自动依赖注入,那这篇文章就来好好聊一下Hilt。

基础介绍

什么是依赖注入

就是类要引用其他类的实例。比如我定义一个类叫做Person,他有一个Name类型的字段name,所以这个Person类就依赖这个Name类型的实例。

data class Person(val name: Name)

data class Name(val firstName: String,
                val lastName: String)
复制代码

Person和Name.png

我们需要Person实例的时候,一般有2种方法来获取Name的实例:

直接构造函数注入

比如我这里的Name类,它是可以通过构造函数来创建实例的,所以得到Name的实例很容易。

字段注入

这啥意思呢,有些类的对象是无法通过构造函数来创建的,比如系统里的Activity实例或者接口类型的实例,那如何做了,就是字段注入。

class Person(){
    lateinit var name: Name
}

data class Name(val firstName: String,
                val lastName: String)

fun test(){
    val person = Person()
    val name = Name("张","三")
    person.name = name

}
复制代码

比如这里name这个字段,就可以在person类创建后,再进行赋值。

自动依赖注入

从前面可以看出,通过代码自行创建依赖项实例并且提供依赖项实例给需要的类,这种方法是手动注入或者人工注入,虽然代码没啥问题,但是如果类的依赖项非常多,而且要严格执行顺序,比如上面例子中在获取打印person.name前必须要实例化好name,这种手动注入的方式就比较容易出错,所以推荐使用自动依赖注入的方式。自动依赖注入目前都只有2个方向:

  • 基于反射的解决方案,可以在运行时连接依赖项
  • 静态解决方案,可生成在编译时连接依赖项的代码

依赖注入的替代方法

对于依赖注入,除了自动依赖注入,我们平时开发中用的最多的就是服务定位器,也就是搞一个单例类,这个类是容器类或者xxManager之类的,这种类的作用就是提供实例。

class Person{
    val name = PersonManager.getName()
}

data class Name(val firstName: String,
                val lastName: String)

object PersonManager{

    fun getName() = Name("张","三")
}

fun test(){
    val person = Person()
    println(person.name)
}
复制代码

这种方式看似不错,但是也有很多致命缺陷:

  • 作用域或者叫做实例的生命周期难以管理,因为很多实例不需要存在在整个生命周期范围内,如果指定一个特定生命周期内,这样又要加很多判断。
  • 依赖项在调用类的地方使用,依赖项实现的地方在统一的类中,这个类可能会很大、很多东西,这样不利于修改和测试。

不论是手动依赖注入,还是服务定位器这种方法,都不太好,所以自动依赖库Hilt就很有必要使用了。

Hilt使用

话不多说,直接开整。

添加依赖

先在项目跟目录添加:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}
复制代码

然后在app的gradle目录和需要用Hilt的moudule都需要添加以下依赖:

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}
复制代码

最后就是项目要求Java 8,在app的gradle中添加:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}
复制代码

Hilt Application

所有使用Hilt的应用都必须先使用@HiltAndroidApp来注解应用的Application,然后会生成一个类,类的格式是Hilt_XXApplication,这个类就是应用级依赖的容器。

同时生成的这个组件,可以给Application提供依赖项,它也是应用的父组件,其他组件可以访问它提供的依赖项。

Hilt Android类

上面说了,我使用@HiltAndroidApp来给Application添加注解后,就可以给Application提供依赖项了,那如何对其他Android类做同样的事呢,这里就需要使用@AndroidEntryPoint这个注解了。

到目前,@AndroidEntryPoint注解能修饰的Android类有以下几种:

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

也就是我使用@AndroidEntryPoint对上面几种Android类添加了注解后,就可以向它里面的字段注入依赖了。

这里注意一点,比如我AActivity依赖于BFragment,所以BFragment使用了@AndroidEntryPoint注解后,AActivity也必须使用该注解。

ok,前面说了2个注解可以让这个类生成一个容器,容器提供依赖,那我定义一个字段,如何标记这个字段的值需要进行依赖注入呢 这个就是@Inject注解执行字段注入。

@AndroidEntryPoint
class LoginMVVMActivity : BaseVMActivity<LoginViewModel>(true) {

    @Inject lateinit var programmer: Programmer

    private val loginViewModel : LoginViewModel by viewModels {
        InjectorUtils.provideLoginViewModelFactory(this)
    }

    override fun getLayoutResId(): Int = R.layout.activity_login_mvvmactivity

    override fun initView() {
        mBinding.setVariable(BR.viewModel,mViewModel)
    }

    override fun initData() {
        Log.i(TAG, "initData: programmer info = ${programmer.toString()}")
    }

}

class Programmer @Inject constructor(){

    var name: String = "张三"
    var age: Int = 20

    override fun toString() : String{
        return "姓名:$name  年龄:$age"
    }
}
复制代码

比如上面这个例子,我有个字段是programmer,但是它没有进行初始化,按理说会报错,但是这里通过依赖注入,会注入一个programmer的实例,在initData方法中并不会报未初始化异常。

Hilt绑定

前面代码我们进行了Hilt Android类,使用@Inject来注解标记哪些字段需要依赖注入,但是如何向Hilt提供这些依赖项的实例呢,这就需要定义Hilt绑定。

向Hilt提供绑定信息的最直接办法就是构造函数注入,在类的构造函数前加上@Inject注解,以告知Hilt如何提供该类的对象实例,比如:

class Programmer @Inject constructor(){

    var name: String = "张三"
    var age: Int = 20

    override fun toString() : String{
        return "姓名:$name  年龄:$age"
    }
复制代码

这里注意的构造函数是没有参数的,如果构造函数有参数,那参数就是该类的依赖项,需要提供参数实例的依赖注入,比如某类是:

class Programmer @Inject construcotr(
    val company: Company){
    ...
    }

复制代码

这种情况下,company字段就默认也是@Inject标记注入了,需要Hilt提供Company对象。

Hilt模块

上面我们通过构造函数注入向Hilt提供该类型的实例方法是非常简单,但是有些类型是无法通过构造函数来得到实例的,这时就需要Hilt一个模块,让这个模块提供该类型的实例,比如现在有个Work接口,表示这个人的工作,但是有不同的实现:

class Programmer @Inject constructor(){

    var name: String = "张三"
    var age: Int = 20
    @Inject lateinit var work: Work

    override fun toString() : String{
        return "姓名:$name  年龄:$age 工作:${work.getWork()}"
    }
}
复制代码

这里多了一个字段是work,这里需要注入,但是看一下Work类型:

interface Work {
    fun getWork(): String
}
复制代码

这里是一个接口,就无法通过构造函数注入来实现,那就需要Hilt一个模块:

class Code @Inject constructor() : Work{
    override fun getWork(): String {
        return "coding"
    }
}

@Module
@InstallIn(ActivityComponent::class)
abstract class WorkMdule{

    @Binds
    abstract fun bindWork(code: Code): Work
}
复制代码

在这个模块中,使用@Module表示Hilt一个模块,使用@InstallIn表示这个模块需要装载到什么地方,具体的范围后面细说,然后这个模块中使用@Binds注解就可以向Hilt提供依赖实例。

这里Hilt模块需要分清楚以下主次关系和提供依赖的逻辑:

  • 模块的作用就是定义一个范围,这个模块要装载在哪里,告诉Hilt我里面有一些提供依赖的方法。
  • @Binds注解定义的函数的返回类型就是告诉Hilt我提供了哪种类型的依赖实现。
  • @Binds注解定义的函数的参数,也是需要依赖注入的。
  • @Binds是用来注入接口实例。

使用@Provides注入实例

前面说了不能使用构造函数注入的类的实例,可以通过Hilt模块来实现,同时使用@Binds来注入接口类型的实例,但是有些类型的实例,比如Retrofit中的APIService,这个就是一系列建造者模式生成的实例,这时需要@Provides来实现。

@Module
@InstallIn(ActivityComponent::class)
object CompanyModule{
    @Provides
    fun provideCompany():Company{
        return CompanyInfo.getCompany()
    }
}

interface Company{
    fun getCompany(): String
}

class HighTech() : Company{
    override fun getCompany(): String {
        return "腾讯"
    }
}

object CompanyInfo{
    fun getCompany(): Company{
       val highTech = HighTech()
        return highTech
    }
}
复制代码

这里注意有个坑点,就是使用@Provides注入实例时,Hilt的模块必须是object修饰的,而不像使用@Binds注入实例的Hilt模块是抽象类。

然后就和上面使用@Binds方式提供依赖一样,可以在需要使用的地方使用@Inject:

@Inject
lateinit var currentCompany: Company
复制代码

为同一类型提供多种绑定

上面说了如何向Hilt提供依赖,以及如何使用依赖,那还有一种情况就是一个类型的多种实现如何做,比如最常见的例子就是OkHttpClient实例,在App中可能会存在多个Client,但是他们类型是一样的,这时就需要使用限定符来区分。

其中限定符就是自定义的注解,还是上面的例子,现在对Company有2种实现,需要区分现在公司和上一家公司,那就定义2种注解:

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

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LastCompany
复制代码

然后在在注入依赖和使用依赖的地方都加上注解:

@Module
@InstallIn(ActivityComponent::class)
object CompanyModule{

    @CurrentCompany
    @Provides
    fun provideCompany():Company{
        return CompanyInfo.getCompany()
    }

    @LastCompany
    @Provides
    fun provideLastCompany(): Company{
        return Shopping()
    }
}
复制代码

在使用的地方:

@Inject
@CurrentCompany
lateinit var currentCompany: Company

@Inject
@LastCompany
lateinit var lastCompany: Company
复制代码

最后打印的结果是:

programmer info = 姓名:张三  年龄:20 工作:coding 目前公司:腾讯  之前公司:阿里巴巴
复制代码

这里代码也非常简单,就是@Qualifier注解来定义新注解,新注解就是限定符,限定它是哪种类型。

Hilt中预定义的限定符

前面说了,限定符就是一种注解,它来区分一种类型,它下面不同的实现,限定这个对象是哪种类型,在Hilt中预定义了2个限定符,分别是@ApplicationContext和@ActivityContext,顾名思义就是对Context这个类型做的区分,是Activity还是Application。

class Programmer @Inject constructor(
    @ApplicationContext val context: Context
)
复制代码
@ApplicationContext
@Inject
lateinit var appContext: Context
复制代码

当然这里的依赖不用自己再去实现了,Hilt帮我做好了。

Android中Hilt生成的组件

上面我们了解了给Android组件注入依赖以及告诉Hilt提供依赖等,那给一个Android类使用@AndroidEntryPoint后会发生什么呢,它是如何做到的去找依赖,以及把依赖注入到Android类中的呢,这里就涉及一个类叫做组件。

当注入器注入Android类时,会生成Hilt组件,也就是注入一个类都会有一个关联的Hilt组件,然后模块会通过@InstallIn注解把模块装载到组件,组件负责将其绑定到相应的Android类。

下面是注入器面向的对象以及关联的Hilt组件:

  • Application -> ApplicationComponent
  • ViewModel -> ActivityRetainedComponent
  • Activity -> ActivityComponent
  • Fragment -> FragmentComponent
  • View -> ViewComponent
  • Service -> ServiceComponent

组件生命周期

既然Hilt了一个Android类,会生成一个Hilt组件,同时Android类都有生命周期,所以Hilt组件也要有生命周期,不然会造成内存溢出。

组件的生命周期如下图:

组件生命周期.png

组件作用域

其实组件作用域就是组件的生命周期范围,所以这里对关联的Hilt组件都定义了一个作用域,比如注入的是Application,它的作用域就是整个生命周期,这里也就相当于是单例,所以每个组件的作用域注解如下图:

作用域.png

那既然有了作用域,对提供的依赖就可以指定其作用域,比如是单例的话就用@Singleton,如果是Activity内单例的话就使用@ActivityScoped:

@Module
@InstallIn(ActivityComponent::class)
abstract class WorkModule{

    @ActivityScoped
    @Binds
    abstract fun bindWork(code: Code): Work
}
复制代码
@Inject lateinit var work: Work
@Inject lateinit var work1: Work
复制代码

在这个例子中,把模块装载到Activity组件中,然后限定提供的依赖作用域也是Activity,这样在这个Activity范围内就只有一个实例。

同时注意,这里提供的依赖的作用域必须和模块装载的组件作用域一致,比如上面例子WorkModule被装载到Activity组件中,那提供的依赖也必须在一样的范围中,如果这里使用别的比如@Singleton来注解bindWork,则会报错。

组件层次结构

上面有了作用域的概念,那肯定就有包含关系,比如一个对象实例它的作用域是整个APP,那我在Activity当然可以访问到,所以这里就有了组件层次结构。

将模块安装到一个组件后,这个模块提供的绑定也就是依赖可以用作其他模块的依赖项,也可以用于组件层次结构中该组件下的任何子组件中其他绑定的依赖项:

组件层次结构.svg

这个也非常容易理解,如果提供的依赖是全APP生命周期,则使用单例注解,它子组件都可以访问到这个实例:

//全局作用域
@Singleton
class Skill @Inject constructor(){
    fun getSkill(): String{
        return "Android"
    }
}
复制代码
//没有定义作用域,虽然每次创建都会有个Code对象,但是这个skill可以从
    //全局组件中获取
class Code @Inject constructor(val skill: Skill) : Work{
    override fun getWork(): String {
        return "coding in ${skill.getSkill()}"
    }
}
复制代码

组件默认绑定

这啥意思呢,也非常好理解,每个Hilt组件都有默认绑定,也就是默认得依赖项,比如我Activity得默认依赖项就有Application,这不是废话吗,所以还是看一下每个组件都有哪些默认绑定:

组件默认绑定.png

此外,前面还说了2个默认限定符来区分Context,所以再加上有默认绑定,所以可以在任何位置直接使用@ApplicationContext来注入App对象,在Activity作用域中使用@ActivityContext来获取Activity上下文。

总结

到这里这一篇的内容先告一段落,主要说了Hilt的使用,当然还不止这些,关于更高级的用法我们就放在下一篇文章里介绍。

具体可以查看: juejin.cn/post/700446… 关于更多和Android更密切的知识。

文章分类
Android
文章标签