Dagger2 是一个由 Google (之前是 Square)维护的开源依赖注入框架。我曾两次试图学习 Dagger 最终被乱七八糟的名词弄得晕头转向,连个 demo 都没写出来就放弃了。所以本文也会重点解释 Dagger 的各个名词,只有熟悉了它们的作用,才能顺畅无阻地使用,也才能看懂别人的 demo。

虽然标题叫 Dagger2 in Android,但是前几节都是 Dagger 通用的基础知识,与 Android 没有关系。

本系列使用 Kotlin 语言,其与 Java 100% 兼容,仅仅是语法不同而已,不会造成太大影响。Kotlin 最终依然会被编译成 jvm 字节码。

例如 java 定义函数: public String getName() {return "David"}

kotlin 版本:fun getName(): String {return "David"},甚至可以更简洁:fun getName() = "David"

依赖注入

那么首先我们得先清楚什么是依赖注入。依赖注入是实现控制反转(IoC)的一个方案。 那什么叫控制反转?别急,我尽量用最通俗的方式解释所涉及的所有概念。

通常在面向对象语言中,一个类往往依赖其他类,一个对象依赖其他对象。那么最直接的方案就是 new 一个出来。这很好理解,我依赖计算机,所以我就自己造一个出来。那么造出的计算机当然是我自己控制的,这就是“控制正转”。那么反转就是,我需要计算机,但是我不自己造,而是厂家造好之后交给我。这就是控制反转。因为我不再控制计算机的生产,此时厂家就叫做 IoC 容器,它负责生产维持对象,而我只负责拿来用。所以 IoC 不是技术,而是思想,利用这种思想可以降低对象间的耦合,提高代码重用率。 倘若计算机的配置发生了变更,在之前每个人都要自己更新图纸,但是现在只需要厂家更新就好了,我总可以拿到最新款的计算机。

为了实现控制反转,有很多方案,常见的有 依赖注入依赖查找服务定位器。这里我们讲依赖注入。

其实依赖注入不是什么新东西,我们天天都在用。依赖注入有三种常见的手段:构造函数Setter注解。举个厨师的例子:厨师依赖炉子。那么代码可以这么写:

// 通过构造函数注入炉子
class Chef(val stove: Stove) {
}

// 通过 Setter 注入
class Chef() {
    var stove: Stove
    
    // 其实 Kotlin 默认实现了 setter,为了更加清晰我手动写了一个。
    fun setStove(stove: Stove) {
        this.stove = stove
    }
}

看到没,依赖注入就在我们身边,我们一直都在用。厨师需要炉子,但他不自己造炉子,而是别人造好后传给他用,也就是「注入」。

Dagger 优势

既然完全可以通过构造函数注入,那为什么要 dagger 呢?当然是因为 dagger 更方便哈哈。

继续厨师的例子,我们知道炉子必须依赖燃料。那么为了得到一个厨师,我们必须先后得到一个燃料、炉子,看下面的代码:

class Stove(val fuel: Fuel) {}

class Fuel(){}

// 创建一个厨师
val fuel = Fuel()
val stove  = Stove(fuel)
val chef = Chef(stove)

看到没,为了得到一个厨师,我们需要创建一堆东西。如果你觉得还不够,其实厨师还依赖菜刀,燃料还依赖天然气。总有一天你会不耐烦。

事实上,有时候2个厨师可以共用1个炉子,而3个炉子可以共用1瓶燃料。这些问题 Dagger 通通可以优雅地解决。怎么样 是不是有点感觉了<( ̄︶ ̄)↗

进入主题

引入 Dagger

Dagger 可以通过多种方式引入,详见 README。作为 Android 我们可以使用 Gradle 声明依赖(kotlin):

dependencies {
    def dagger_version = "2.23.1"
    implementation "com.google.dagger:dagger:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:$dagger_version"
    
    // 如果需要使用 Android 的特有 Dagger 功能,还要引入下面的库
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"
}

@Inject

@Inject 是我们接触到的第一个 Dagger 注解。它有两个作用:① 标注哪些东西需要注入。② 标注这些东西怎么创建。

假设 Dagger 是后勤管理部,那么作为厨师,你必须告诉 Dagger 需要哪些东西,然后还要告诉他这些怎么造,这样 Dagger 才能注入给你。所以厨师和炉子(简单起见忽略燃料)我们可以这样改写:

// 告诉 Dagger 炉子可以通过一个无参的构造函数造出来
class Stove @Inject constructor() {}

class Chef() {
    @Inject // 告诉 Dagger 我需要一个炉子
    val stove: Stove
}

就是这么简单。现在 Dagger 已经知道我们需要什么、这个东西怎么创建了。这里我把厨师称为目标类,也就是「炉子要往哪注入」,这就是目标。

@Component

之前我们已经让厨师和炉子建立了无形的联系。但是要真正获得炉子,但是还得让这个联系直接一点。毕竟之前那都是泛泛而谈,针对具体的一个厨师(实例)我们得具体操作。这个桥梁就是 @Component。Component 将具体连接厨师所依赖的炉子,和炉子的构造函数。

Component 是一个注解类,且是一个接口或抽象类。因此必须对一个接口标记 @Component 才能获得一个 Component。它将获得一个目标类实例的引用(也就是一个活生生的厨师),然后查找这个类所依赖的类(询问厨师你需要啥)。得到答案之后它会去查找是否有已知的构造函数(之前 @Inject 标注过的),然后实例化并注入到目标类(造个炉子交给厨师)。

根据上面的解释,我们可以轻松写出一个 Component:

@Component
interface MainComponent {
    // 定义一个函数,以便拿到目标类实例的引用
    fun inject(chef: Chef)
}

其实到目前为止我们已经实现一个完整的依赖注入了:告诉 Dagger 厨师需要炉子、告诉炉子应该怎么造,并且建立了一个直接的桥梁。

恭喜!(。⌒∇⌒)

@Module

这又是什么鬼呢?我们来想想,如果厨师也不会造炉子咋办。 反应到项目中就是,我们引入了第三方库,这个库没有在构造函数上标记 @Inject,总不能修改源码自己加上吧。这时候就要 Module 出场拉。Module 相当于给第三方库套一层封装,给他从外面包裹一个能够通知 Dagger 的创建方法。比如厨师不会造炉子,但是他知道去哪买,那么这也是OK的。

按照这个思路我们修改一下厨师炉子的代码:

// 现在去掉炉子的 @Inject 代表厨师不会造炉子
// 假设炉子是第三方库,我们无权添加 @Inject
class Stove(){}

@Module
class MainModule() {
    // 虽然我不会造,但我可去 [MainModule] 这个商店买
    provideStove():Stove {
        return Stove()
    }
}

Module 就像工厂模式,里面提供了各种创建实例的方法。

现在我们必须让 @Component 知道 Module(商店)的存在。非常简单:

// 直接传入 Mudoule 类数组就好啦
@Component(modules = [MainModule::class])
interface MainComponent {
    fun inject(chef: Chef)
}

然后有一个新问题,之前利用 @Inject 标注了类的构造方法,现在 @Module 只标注了一个商店,并没有指明某个具体的类(炉子)到底怎么获得。就好像现在后勤管理处知道去家乐福可以买个炉子,但是不知道具体在哪个区域。

[注] 一个 Module 可以被多个 Component 引用。因为有可能多家超市都卖炉子。

@Provides

Provides 将最终解决第三方库的问题。我们把 Module 中所有创建实例的函数都用 Provides 标注。那么这些函数就是会 Dagger 所识别然后选择一个返回值类型匹配的进行调用。

现在对于厨师依赖的第三方炉子,Dagger 将这样处理:首先桥梁告诉他有个商店(MainModule)可能有炉子,于是到商店筛选所有货架(Provides),找到匹配的商品带回来即可。

@Module
class MainModule() {
    @Provides // 加上一个注解表明这个函数可以提供一个炉子(或其他物品)
    provideStove():Stove {
        return Stove()
    }
}

到目前为止,我们已经了解了 Dagger 的基础内容。我们已经学会了如何通知 Dagger 所需的依赖、依赖类如何创建,以及对于第三方提供的类该如何包装并使 Dagger 识别。

希望厨师的类比能帮助你更好地理解相关概念。下面我们将继续学习其他注解

Last modification:June 9, 2019