使用Hilt完成依赖注入,让你的安卓代码层次有几层楼那么高(二)

2,870 阅读6分钟

上一篇文章的快捷入口:使用Hilt完成依赖注入,让你的安卓代码层次有几层楼那么高(一) - 掘金 (juejin.cn)

使用Hilt注入不能修改构造方法的类

  在上一节中,在向Hilt说明如何注入需要的类的时候,我们是通过直接修改类的构造方法的方式来实现的,那么实际开发中存在多种情况,是我们无法修改构造方法的,那么如何完成这个任务呢?答案是自定义我们的模块,这里的模块指的是带有 @Module 注释的类,这个模块的意义跟@Inject注释加在构造函数中的意义是一样的,都是向Hilt说明注入对象的方式

1.注入一个接口(Bind方式)

假设一个这样的场景,activity需要使用一个TestInterface的接口实例,实现则来自TestImpl,按照第一章的方法,我们无法使用@Inject去修饰接口的构造参数,因为接口根本没有构造参数。

@AndroidEntryPoint
class ExampleActivity:AppCompatActivity() {

    @Inject
    lateinit var testInterface: TestInterface

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        testInterface.doSomeThing()
    }

}

interface TestInterface{
    fun doSomeThing()
}

//接口实现
class TestImpl:TestInterface{
    override fun doSomeThing() {
        Log.d("日志","做了一些事")
    }
}

  于是,我们新建一个Hilt容器,使用@Module注解,注意容器必须是抽象类。然后我们新建一个方法,方法也必须是抽象方法,方法使用@Bind注解,方法参数为接口的实现,方法的返回值为我们需要注入的方法。

@Module
//引用生成在Activity上的组件,关于这个后续文章细讲
@InstallIn(ActivityComponent::class)
abstract class TestModule {

    @Binds
    abstract fun bindTestInterface(
        //接口的实例
        testImpl: TestImpl
    ): TestInterface //需要绑定的接口
}

  写完容器之后,接口左侧代码的位置出现了我们熟悉的小图标,点击图标,发现跳转的正是我们刚刚实现的容器的代码块区域,这反映了接口的注入源是我们自己实现的容器。

image.png

2.注入一个不能修改的类(Provide方式)

  不能修改的类是什么意思呢,也就是说不是本项目创造的类,我们并不能直接修改其构造方法,例如String、okhttp、Gson等,和注入接口一样,我们同样是创造Hilt容器的方式,和注入接口有点不同,容器不能是抽象类,因为我们需要具体的方法体,方法使用@Provide注解。

  方法的参数是构建当前对象需要的其他依赖(待会会讲),方法的返回值是我们要注入的类型,方法体则是注入的方式,下面以okhttpClient为例子。

@AndroidEntryPoint
class ExampleActivity:AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
    }

}

@Module
@InstallIn(ActivityComponent::class)
object OkHttpClientModule{
    
    @Provides
    fun provideOkHttpClient():OkHttpClient{
        return OkHttpClient()
    }
    
}

  刚才上面提到了,Provide方法的参数是构建当前对象需要的其他依赖,这是如何理解呢,例如我们在构建OKhttpClient的时候,肯定是需要其他参数的,我们以cookiejar为例子展示如何在Provide方法中传入其他参数。

  假设我们有一个这样的CookieJar,我们需要把他传入到OKhttpClient构造过程中去,首先我们需要向Hilt声明如何构建一个CookieJar实例,根据第一章以及第二章的内容,我们有三种方式来完成:

1.在构造函数上添加一个注解
2.使用Binds方式
3.使用Provides方式

//需要被注入到OkHttpClient中的CookieJar实例
class TestCookiejar : CookieJar{
    override fun loadForRequest(url: HttpUrl): List<Cookie> {
        TODO("Not yet implemented")
    }

    override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
        TODO("Not yet implemented")
    }
}

下面将分别演示三种方法的代码:

1.构造函数上添加注解

class TestCookiejar @Inject constructor(): CookieJar{
    //...略
}

2.Binds方式

@Module
@InstallIn(ActivityComponent::class)
abstract class CookieJarModule{
    @Binds
    abstract fun bindsCookieJar(testCookiejar: TestCookiejar):CookieJar
}

3.Provides方式

@Module
@InstallIn(ActivityComponent::class)
object CookieJarModule2{
    @Provides
    fun providesCookieJar():CookieJar{
        return TestCookiejar()
    }
}

  你可以选择以上任意一种,他们的目的都只有一个:向Hilt说明你要如何构建一个CookieJar的实例,选择其中一个放入到代码中,然后我们回到OkHttpClient的容器代码中,在方法中添加一个CookieJar参数。

image.png

  添加了参数之后,参数左侧又出现了一个小图标,说明方法中的这个参数已经成功被容器提供了,一切都不需要我们手动传入,只需要向Hilt说明即可。同样的,我们回到OkHttpClient在Activity是如何被获取的。

image.png

  没有发生任何变化!因为我们只需要告诉Hilt:我要在某个Activity中获取一个OkHttpClient,一切的都交给Hilt即可,哪怕OkHttpClient的构建过程发生了变化(例如新增了一个CookieJar),对于最上层的获取者(Activity)来说,都是无感的,非常的轻耦合!

一个类,多种注入方式

  上述的多种注入方式都存在一个问题,我们只能对一个类注入一种实例,例如OkHttpClient只能存在一种,但是实际项目中可能存在多套Client,亦或者是更常见的String,我们总不能只能注入一个String对象吧,因此我们需要告诉Hilt如何区分它们。

方法一:使用Named注解

非常的简单,只需要在Provides方式的容器的方法中增加一个注解Named,然后写上独一无二的Tag,在使用端同时加上对应的Tag,这样Hilt注入的时候就会选择对应的注入方式,为同一个类型提供多种绑定。

@AndroidEntryPoint
class ExampleActivity:AppCompatActivity() {

    @Inject
    @Named("One")
    lateinit var oneString:String
    
    @Inject
    @Named("Two")
    lateinit var twoString:String

}

@Module
@InstallIn(ActivityComponent::class)
object StringModule{
    
    @Provides
    @Named("One")
    fun providesOneString():String{
        return "One"
    }
    
    @Provides
    @Named("Two")
    fun providesTwoString():String{
        return "Two"
    }
    
}

注入接口也是同样的道理,在对应的binds方法中添加Named注解即可,你可以自己动手试试

方法二:自定义注解

首先笔者说下自己的态度,笔者并不喜欢第一种方式是因为Named注解里面的值只能硬编码,由于注解的特性并不能传入一个静态的String,因此容易写错或者后期重构容易遗漏修改导致出现注入失败,因此笔者更喜欢这种方式。

首先根据你需要的分类定义相应的注解,注解都添加@Qualifier(告诉Hilt这个注解是分类器)

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

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

其他的部分和Named差不多

@AndroidEntryPoint
class ExampleActivity:AppCompatActivity() {

    @Inject
    @OneString
    lateinit var oneString:String

    @Inject
    @TwoString
    lateinit var twoString:String

}


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

    @Provides
    @OneString
    fun providesOneString():String{
        return "One"
    }

    @Provides
    @TwoString
    fun providesTwoString():String{
        return "Two"
    }

}

好了,这篇文章到这里就结束了,下一期我们继续探讨一下Hilt的容器生命周期和容器作用域相关的话题,特别是如何让注入的对象单例化。

下一期入口:使用Hilt完成依赖注入,让你的安卓代码层次有几层楼那么高(三) - 掘金 (juejin.cn)