跳转至

初学者的Dagger2教程

最近新开了个项目,想搞点事情。

在原来的项目中,初始化并绑定ViewModel、DataBinding是靠泛型来实现的,基类声明如下:

abstract class BaseActivity<VM : BaseViewModel, B : ViewDataBinding> : AppCompatActivity()

虽然用着很愉快,基类里面的ViewModel和DataBinding都是传入的类型:

protected lateinit var viewModel: VM
protected lateinit var binding: B

但是在某些类里面使用基类时,看起来令人不爽,比如:

if (activity is BaseActivity<*, *>) {
    hostActivity = activity
}

所以就起了想法,想用Dagger2来替换这看起来不爽的泛型。虽然替换后也不太爽,但是Dagger2有用起来了啊,这就够了。

下面正式开始Dagger2教学。

Dagger2就是一个依赖注入框架。使用时先添加对应依赖:

implementation "com.google.dagger:dagger:2.21"
kapt "com.google.dagger:dagger-compiler:2.21"

因为项目全部使用的Kotlin,所以这里使用的是kapt而不是Java中的annotationProcessor,另外kapt需要在apply plugin: 'kotlin-kapt'一下。

1. 基本注入

首先是最简单、最基本的无参对象的注入。

比如,我家的小狗渴了,想喝些什么。

为了让它可以喝到Dagger味的,我们可以这样在Dog类的无参构造函数上加一个@Inject注解。

class Dog @Inject constructor() {
    fun drink() = "drink"
}

使用的时候可以通过@Inject注解来完成注入。

class DaggerActivity : BaseActivity() {

    @Inject
    lateinit var dog: Dog

    ...
}

需要注意的是,@Inject只是一个注解,我们还需要一个用来对注解进行处理的处理器。这个处理器就是我们刚开始kapt的com.google.dagger:dagger-compiler

现在我们需要写一个额外的代码,告诉注解处理器如何处理该注解,这个额外的代码中,@Component注解是关键。

我们可以写出下面的这种注入器:

@Component
interface DogComponent {
    fun createDog(): Dog
}

该注入器提供了一个创建DogcreateDog方法。使用时如下使用:

dog = DaggerDogComponent.builder().build().createDog()

当然,使用上面这种方法时,@Inject lateinit var dog: Dog中的@Inject是不需要的。因为这里只是使用dagger创建了对象,而没有自动完成注入功能。

所以,这里推荐第二种注解器的编写方式:

@Component
interface DogComponent {
    fun inject(activity: DaggerActivity)
}

DaggerActivity 中使用时 ,通过下面的代码完成自动注入:

DaggerDogComponent.builder().build().inject(this)
// 等价于 DaggerDogComponent.create().inject(this)

自动注入了Dog之后,我们就能让它喝些什么了:

tv.text = dog.drink()

我们加粗强调了 DaggerActivity 中使用 ,这是因为Dagger是强类型的,在哪个类中使用,就只能声明inject哪个类。如果我们把上面声明中的DaggerActivity换成基类BaseActivity,编译会通过,但是一运行就会报错。另外,在builder时如需要设置Module,但是没有声明inject此类,设置Module的方法会生成为@deprecated类型。

2. 对象注入

上面一节中,我们已经命令doge喝了,但是很遗憾,没有什么东西可以让它喝。我们认识到了错误,并赶紧提供了一杯Water

首先我们需要写一个Water类,由于这是被Dog需要的。所以,Water需要有一个@Inject的无参构造方法,然后在原来Dog的构造方法中直接加上water参数就可以了。

@Component部分和DaggerActivity中的代码无需任何改动,多么amazing。

看看改动后的WaterDog代码:

class Dog @Inject constructor(
    private val water: Water
) {
    fun drink() = "drink $water"
}

class Water @Inject constructor() {
    override fun toString() = "water"
}

经过上面的改动后,我们再次运行,就可以看到Dog已经喝到Water了。

此外,需要注意,基于上面的代码,如果我们在DaggerActivity中新@Inject一份Water,那么这两份Water并不是同一个Water。那么,如何得到一个Singleton的Water呢,可以使用@Singleton注解,下面一节会提到。

3. 模型与单例

在上面两节中,我们需要注入的对象都是自己可以改动的,如果遇到第三方源码里的对象需要注入,那么怎么办。此时,我们可以使用@Module注解,搭配@Provides注解使用。

我们拿OkHttp为例,顺便说下@Singleton注解的使用方法。

首先实现OkHttp的提供者@Provides,而@Provides必须处于@Module修饰的类中,我们我们可以先写出下面的代码:

@Module
class OkHttpModule {
    @Provides
    fun provideOkHttp() = OkHttpClient()
}

这样我们就赋予了Dagger创建OkHttp的能力,接下来要把这个Module添加到@Component中:

@Component(modules = [OkHttpModule::class])
interface OkHttpComponent {
    fun inject(activity: DaggerActivity)
}

编译之后就能注入到DaggerActivity中了:

class DaggerActivity : BaseActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        DaggerOkHttpComponent.create().inject(this)
    }
}

另外,OkHttpClientRetrofit都要单例实现,这样更高效。所以,我们改造一下,提供单例能力,只需要在@Provides和对应的@Component上添加@Singleton注解就可以了:

@Module
class OkHttpModule {
    @Singleton
    @Provides
    fun provideOkHttp() = OkHttpClient()
}

@Singleton
@Component(modules = [OkHttpModule::class])
interface OkHttpComponent {
    fun inject(activity: DaggerActivity)
}

怎么验证呢,我们可以在DaggerActivity中直接声明两个OkHttpClient,然后在注释掉@Singleton前后,打印其的toString方法返回的值,观察其hashcode的16进制表示是否相同即可。

需要注意,只对同一次Inject有效。如果在Application中inject一次,然后在页面中inject一次,这两次还是会生成不同的对象。

4. 带参注入

接着第二节,我们谈到了狗喝水。但是狗有很多啊,让哪一只喝水呢?加一个年龄参数吧。

Dog就变成这样了。

class Dog @Inject constructor(
    private val age: Int,
    private val water: Water
) {
    fun drink() = "$age years old dog wanna drink $water"
}

看这里两个参数,在前面可以知道,water: Water是已经可以被@Inject了的,剩下的age: Int怎么办。这是一个基本类型,没有任何可以直接@Inject的机会。
既然没有@Inject的机会,那么我们就@Provides嘛。只要能满足一个注解即可。

所以,我们创建一个带需要被Provides的参数age的类DogModule,然后将该参数@Provides出去,供下面的@Provides方法创建Dog使用:

@Module
class DogModule(
    private val age: Int
) {
    @Provides
    fun provideAge() = age

    @Provides
    fun provideDog(water: Water) = Dog(age, water)
}

同第3节,这里的Module也需要加入到Component中:

@Component(modules = [DogModule::class])
interface DogComponent {
    fun inject(activity: DaggerActivity)
}

调用Dagger进行注入:

DaggerDogComponent.builder().dogModule(DogModule(10)).build().inject(this)

这样就将参数10传入到Dog中。

在第一节说道,Dagger是强类型的。如果没有inject到,会出现@deprecated的提示,建议此处将DogComponentinject参数换成AppCompatActivity重新编译看看dogModule方法的提示。

5. 多Module注入

目前为止,我们已经有两个Module了:DogModule以及OkHttpModule

如果我们想在DaggerActivity中顺便使用OkHttp,那么我们可以在DogComponent的注解参数中将OkHttpModule加进入,这样在DaggerActivity中就可以@Inject lateinit var okHttpClient: OkHttpClient来注入了。下面是DogComponent的代码。

@Component(modules = [DogModule::class, OkHttpModule::class])
interface DogComponent {
    fun inject(activity: DaggerActivity)
}

又或者,我们的Dog也想上网OkHttpClient,那么我们可以满足

  • 首先在Dog中加上对应的字段,同时在构造器中也新增该字段
  • DogModule.provideDog的参数和实现中加入该字段
  • 最后,在DogComponent的注解参数中将OkHttpModule加进入

代码如下:

class Dog @Inject constructor(
    private val age: Int,
    private val water: Water,
    private val okHttpClient: OkHttpClient
) {
    fun drink() = "$age years old dog wanna drink $water, $okHttpClient"
}

class Water @Inject constructor() {
    override fun toString() = "water"
}

@Module
class DogModule(
    private val age: Int
) {
    @Provides
    fun provideAge() = age

    @Provides
    fun provideDog(water: Water, okHttpClient: OkHttpClient) = Dog(age, water, okHttpClient)
}

@Module
class OkHttpModule {
    @Provides
    fun provideOkHttp() = OkHttpClient()
}

@Component(modules = [DogModule::class, OkHttpModule::class])
interface DogComponent {
    fun inject(activity: DaggerActivity)
}

使用的时候如下:

DaggerDogComponent.builder()
            .dogModule(DogModule(10))
            .okHttpModule(OkHttpModule())   // 此行可以省略
            .build()
            .inject(this)

6. @Named@Qualifier

回到第4节之后,这时候我们可以让多大的DogWater了;但可能有些Dog刚喝了什么,现在什么都不想喝。

这时候我们可以使用@Named或者@Qualifier注解来让客户端选择注入想要的Dog

首先说说@Named注解如何操作。

提前小改一下Dog类,当构造时不传参数,就认为不喝水了:

class Dog {
    constructor()

    constructor(age: Int, water: Water) {
        this.age = age
        this.water = water
    }

    private var age: Int? = null
    private var water: Water? = null

    fun drink() = if (age != null) "$age years old dog wanna drink $water" else "no no no i don't wanna"
}

然后在DogModule中提供一个不需要喝水的provideDogA方法;同时为了区分客户端需要的Dog,需要在对应提供了创建Dog能力的方法上加上@Named注解:

@Module
class DogModule(
    private val age: Int
){
    @Provides
    fun provideAge() = age

    @Named("a")
    @Provides
    fun provideDog(water: Water) = Dog(age, water)

    @Named("b")
    @Provides
    fun provideDogA() = Dog()
}

注意:并不是所有创建Dog的方法都需要加上@Named注解,没有注解的可以匹配上没有注解的。比如,在这里可以删掉@Named("b")注解,注入的客户端也不加@Named("b")注解,这样可以匹配上。
同时,需要注意,在同一个@Module里面不能有同样函数名的@Provides,即使函数签名不一样,否则会报错:error: Cannot have more than one @Provides method with the same name in a single module

最后在客户端注入就行了:

@Inject
@field:Named("a")
lateinit var dog: Dog

@Inject
@field:Named("b")
lateinit var dogA: Dog

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    ...
    DaggerDogComponent.builder()
        .dogModule(DogModule(24))
        .build()
        .inject(this)
}

注意:在客户端注入的时候,直接加上注解@Named("b")是不OK的,正确的写法是@field:Named("b")。作者在这里卡了很久,一直在某天晚上11点想起来了……

下面接着看@Qualifier注解

@Named注解里面的value得是一个String,虽然 可以传入静态字符串常量 ,但还是感觉太Kotlin,不JAVA。

所以这里看看@Qualifier的用法,首先我们需要使用该注解为两种不同的Dog编写两个不同的注解类:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class DrinkWaterDog

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class DrinkNothingDog

然后使用上面的注解替换掉DogModule以及客户端里面的@Named注解:

@Module
class DogModule(
    private val age: Int
){
    @Provides
    fun provideAge() = age

    @Provides
    @DrinkWaterDog
    fun provideDog(water: Water) = Dog(age, water)

    @Provides
    @DrinkNothingDog
    fun provideDogA() = Dog()
}

// 客户端
class DaggerActivity : BaseActivity() {

    @Inject
    @field:DrinkWaterDog
    lateinit var dog: Dog

    @Inject
    @field:DrinkNothingDog
    lateinit var dogA: Dog

    @SuppressLint("SetTextI18n")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ...
        DaggerDogComponent.builder()
            .dogModule(DogModule(24))
            .build()
            .inject(this)
    }
}

7. 官网例子

官网例子是一个冲咖啡的小例子,先用Heater加热,然后Pump冲,最后就得到咖啡了:

// 1--Heater
interface Heater {
    fun on()
    fun off()
    fun isHot(): Boolean
}

class ElectricHeater : Heater {

    private var heating = false

    override fun on() {
        println("~ ~ ~ heating ~ ~ ~")
        heating = true
    }

    override fun off() {
        heating = false
    }

    override fun isHot() = heating
}

// 2--Pump
interface Pump {
    fun pump()
}

class Thermosiphon @Inject constructor(
    private val heater: Heater
) : Pump {
    override fun pump() {
        if (heater.isHot()) {
            println("=> => pumping => =>")
        }
    }
}

// 3--CoffeeMaker
class CoffeeMaker @Inject constructor(
    private val heater: Lazy<Heater>,
    private val pump: Pump
) {
    fun brew() {
        heater.get().on()
        pump.pump()
        println(" [_]P coffee! [_]P ")
        heater.get().off()
    }
}

// 4--Module
@Module
abstract class PumpModule {
    @Binds
    abstract fun providePump(pump: Thermosiphon): Pump
}

@Module(includes = [PumpModule::class])
class DripCoffeeModule {
    @Provides
    @Singleton
    fun provideHeater(): Heater = ElectricHeater()
}

// 5--Component
@Singleton
@Component(modules = [DripCoffeeModule::class])
interface CoffeeShop {
    fun maker(): CoffeeMaker
}

// main
fun main() {
    val coffeeShop = DaggerCoffeeShop.builder().build()
    coffeeShop.maker().brew()
}

运行结果:

~ ~ ~ heating ~ ~ ~
=> => pumping => =>
 [_]P coffee! [_]P 

我们分析下源码的运行流程中其中还涉及到@Binds注解和Lazy接口。

首先1、2两个部分定义了HeaterPump接口以及其实现类;第3部分CoffeeMaker利用上面两个接口实现了咖啡的制作逻辑。然后看main部分,这里调用了DaggerCoffeeShop获得了一个CoffeeMaker并开始了制作。所以关键在4、5部分。我们溯源一下。

首先看CoffeeMaker,其构造器被@Inject修饰了,所以这里面的HeaterPump要不其构造器也是@Inject的,要不就是有@Module为其@Provides或者@Binds了。

先看看Pump的创建过程。
在第4部分的PumpModule中提供了Pump,注意这里的语法:

@Binds abstract fun providePump(pump: Thermosiphon): Pump

该方法是抽象类中的抽象方法,不像其他Module中的@Provides会给出创建过程,这里啥都没干,怎么创建了Pump对象了呢。
关键肯定是@Binds,这里告诉Dagger使用pump: Thermosiphon来创建Pump。所以回过头来看一下Thermosiphon的构造器,果然被@Inject修饰了。

这里Pump就被创建了。接着看Heater的创建。
先看类的定义,Heater以及ElectricHeater都没有被@Inject,那么肯定是在@Module里面。接着看DripCoffeeModule,这里果然@Provides了一个Heater,还是个ElectricHeater
另外注意一下,该Module还includesPumpModule,这样在@Component中就不需要再次包含PumpModule了。

现在1、2、3里面的类都组织了起来,Dagger就能愉快的工作了。总结一下第5部分这个@Component的流程。
首先在注解中指定了modules为DripCoffeeModule,而DripCoffeeModule又include了PumpModule,这样Component就能组织两个Module里面的HeaterPump了。
然后客户端调用Dagger构建CoffeeMaker,这类@Inject了两个参数:HeaterPump,OK,都是满足的。这样,就能愉快的制作咖啡了。

最后看一下Lazy接口的用法,很简单:声明的时候使用Lazy<T>进行声明;使用时调用其get方法即可。


最后更新: 2020年1月14日

评论