Dagger2 in Android(一)通俗基础

2,013 阅读7分钟

系列文章

背景知识

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 识别。

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