这可能是本年度最好用的 Dagger 使用教程对于依赖注入来说,Dagger 这个库是绕不开的。本文通过一个生动的例子,
依赖注入
在介绍 Dagger2 这个之前,必须先解释一下什么是依赖注入,因为这个库就是用来做依赖注入的。所以这里先简单介绍一下依赖注入的概念:
控制反转(Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称DI),还有一种方式叫“依赖查找”(Dependency Lookup)。通过控制反转,对象在被创建的时候,由一个调控系统内所有对象的外界实体,将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
Dagger2
上面的博客已经介绍了,依赖注入有很多不同的框架都可以做这个事,那为什么选择 Dagger2 呢?对于后端开发可能会用 Spring,而对于 Android 开发,只能用 Dagger2 了。这主要是因为这个库本身就是由 Google 推出的,而且它通过注解处理器生成高效的依赖注入代码,避免了运行时反射产生的性能开销。在 Android 源代码项目中,广泛使用了这个库。
这里重点注意 Dagger2 这个库与其他依赖注入库的区别在于 Dagger2 使用的是注解处理器,而不是运行时反射。如果不了解这两个方式的区别可以看一下这个:
使用注解处理器可以在编译时生成代码来完成功能,这比使用运行时反射要快很多。而性能在 Android 这种嵌入式设备中相当重要,因此对于 Android 开发者来说,如果使用依赖注入,这个库就是必选的。
基本概念
在使用 Dagger2 这个库进行依赖注入时,有三个重要角色:
- 依赖需求方:就是需要依赖对象的那些类。 例如一个人想要玩电脑,那么他就必须得有一台电脑,电脑是依赖对象,这个人依赖于这台电脑,因此这个人就是依赖需求方;
- 依赖供应方:负责提供依赖对象,类似与实际编码中的工厂类。 这个人依赖一台电脑玩游戏,那么就必须有个地方能够提供一台电脑,他可以去实体店买,也可以去网上买,而这个能向这个人提供电脑的地方就是依赖供应方。顾名思义,就是创建依赖对象的地方;
- 依赖注入器:负责将依赖对象注入到依赖需求方,在实际代码中是一个接口,编译时 Dagger2 会自动生成这个接口的实现类。 接着上面的说,这个人是依赖需求方,他需要一台电脑,依赖供应方能够提供一台电脑,可是这两者没有打通,电脑没有给到这个人,他还是玩不了游戏啊,因此这个时候就需要依赖注入器将这台电脑注入给这个人,他就能够使用这台电脑了。
上面已经说得很形象了,大家应该都能理解,这里在啰嗦一下,构建一个场景,后续使用 Dagger 来实现这个场景:
你需要一台电脑打游戏,那么你依赖于电脑,你就是依赖需求方,依赖对象是一台电脑。这台电脑哪里能提供呢?当然是淘宝、京东、实体店了,这些地方都能提供一台电脑,因此它们就是依赖供应方。但是这中间必须得有个东西把电脑从供应商的仓库送到你手里,你才能用,这就可以理解为将电脑这个依赖对象注入到你手中。什么是依赖注入器呢?在这个购物的场景中,那就是三通一达这些快递公司了。
就是一个简单的购物的流程,只是把依赖注入的概念套进去了而已。下面我们就以这个场景为例,写个 Demo,告诉大家如何使用 Dagger2 这个库。
引入 Dagger2
截止到目前,Dagger2 这个库的最新版本是 2.51.1。引入这个库的方式也很简单,在 build.gradle 中添加如下依赖:
dependencies {
implementation 'com.google.dagger:dagger:2.51.1'
annotationProcessor 'com.google.dagger:dagger-compiler:2.51.1'
}
大家在添加依赖的时候最好查看一下 Dagger 的官网,引入最新的版本。
Dagger 的官方网站:dagger.dev/
在引入依赖并 Sync Project 之后,你会发现项目的依赖会多出来以下两个库:
编写依赖需求方
先编写一个 Person
类,里面有一个 playGame
的方法,这个方法中要使用 Computer
,也就是说,Computer
是 Person
的依赖,因此我们使其成为一个成员变量:
public class Person {
private String name;
private Computer computer;
public void Person(String name) {
this.name = name;
}
public void playGame(String gameName) {
System.out.print(name + "\n\t");
computer.play(gameName);
}
}
以下是 Computer
类,作为依赖对象,后面我们将通过 Dagger 设置到 Person
中的 computer
成员变量:
public class Computer {
private String name;
public Computer(String name) {
this.name = name;
}
public void play(String game) {
System.out.println("使用 " + name + " 玩 " + game);
}
}
编写依赖供应方
现在,有了依赖需求方,那我们就要找到依赖提供商,并从依赖供应方拿一台电脑。哪里能提供电脑呢?京东、淘宝、实体店都行,这里就先编写一个淘宝类吧:
@Module
public class TaoBao {
private Computer assembleComputer() { //组装一台电脑
Computer computer = new Computer("淘宝组装的电脑");
return computer;
}
@Provides
public Computer getComputer() {
return assembleComputer();
}
}
这里注意两个注解 @Module
和 @Provides
,这两个注解是 Dagger 提供的。
@Module
用于告知 Dagger 这个类是一个依赖提供商,这样 Dagger 才能够识别。
@Provides
用于告知 Dagger 这个依赖提供商里面哪些方法是用于提供依赖对象的。当 Dagger 需要创建一个依赖对象时,它会查找被 @Module
标识的类中被 @Provides
标识的方法,并根据所需依赖对象的类型,来看这里面哪个方法返回的是所需要的依赖对象的类型并调用。在这个例子中,Dagger 要创建一个 Computer
对象,那么就要看哪个方法返回的是一台电脑,而不是一个鼠标或啥的。
在一个方法上声明 @Provides
注解,就相当于这个供应方里面有了一个依赖对象的生产线,这条生产线的产物就是方法的返回值类型。有了这条生产线,供应商就能够提供这种类型的对象了。当 Dagger 看到有哪个需求方需要这个类型的对象时,就可以调用这个方法创建依赖对象并注入给依赖需求方了。
在这个例子中, TaoBao
是一个依赖供应方,其中 getComputer
用于提供依赖对象,assembleComputer
则是一个普通方法。
编写依赖注入器
有了需求方和供应方,那么就需要将两者连接起来,依赖对象只有从供应方交给需求方,才有意义。这就像厂商生产的商品只有卖给用户,才能发挥商品的作用,经济才能发展。
那么连接这两者的这就是依赖注入器的工作。在这个例子中,依赖注入器就是快递了,快递把电脑从淘宝店家送到买家手中。这里我们就先编写一个中通吧:
@Component(modules = TaoBao.class)
public interface ZTOExpress {
void deliverTo(Person person);
}
注意依赖注入器是一个 interface
而非 class
,在编译时,Dagger 会生成对应的实现类。
这个接口添加了一个注解:@Component
,这个注解是就是告诉注入器,从哪个依赖供应方拿依赖对象。这段代码里,@Component
注解告知了中通,去淘宝这个供应商拿到电脑并快递给买家。
@Component
这个注解的 modules
属性是一个 Class<?>[]
数组,因此可以让依赖注入器指定不止一个依赖供应方。例如,这个例子中,中通不仅可以从淘宝拿电脑进行配送,也可以从京东拿电脑:
@Module
public class JD { //定义京东这个供应商
@Provides
public Computer getComputer() {
return new Computer("从京东购买的电脑");
}
}
//可以从淘宝和京东两个地方获取依赖对象
@Component(modules = {TaoBao.class, JD.class})
public interface ZTOExpress {
void deliverTo(Person person);
}
但是这里要注意不要重复,如果淘宝和京东都有函数返回 Computer
,那么 Dagger 就不知道从哪个供应商拿。这个时候需要考虑哪个供应商提供,或者使用 @Qulifier
或 @Named
注解,这个后面会讲。
这里还有最后一个问题,中通知道将电脑配送给买家,但是你得将配送的地址给到中通啊。也就是配送给哪个 Person
对象,并配送给这个 Person
对象的哪个成员变量呢?
这里就得介绍 @Inject
注解了。Person
类里有 name
和 computer
两个成员变量,从名字上就能看到电脑肯定要配送到 computer
的成员变量上,这个时候需要将 computer
这个成员变量添加 @Inject
注解。这时候 Dagger 就知道 Computer
对象要赋值到这个成员变量上,而至于配送给哪个 Person
,那是我们自己指定的,看后续的代码。
public class Person {
private String name;
@Inject
private Computer computer;
//......
}
依赖注入结果
现在三个角色都有了,那我们现在就把它们拼接在一起,看看效果吧。
//创建一个爱玩游戏的张三
Person person = new Person("张三");
//创建依赖注入器
ZTOExpress ztoExpress = DaggerZTOExpress.builder().taoBao(new TaoBao()).build();
//通过中通这个依赖注入器,为张三提供一台电脑
ztoExpress.deliverTo(person);
//现在张三可以玩游戏了
person.playGame("赛博朋克2077");
这里注意一点,Dagger 生成的依赖注入器,其名为 Dagger+定义的接口名 ,生成类使用了 Builder 设计模式,在这里需要将依赖供应方设置进去,最终 build 才能生成一个依赖注入器并使用。上面的中通就是这么创建的。
看一下输出:
System.out I 张三
System.out I 使用 淘宝组装的电脑 玩 赛博朋克2077
@Inject 用于构造方法
对于初学者来说,上面的例子给人最大的感受应该就是麻烦了吧。只不过为了给 Person
的 computer
赋个值,还要搞什么 @Module
类,搞什么 @Provides
方法,还要搞什么 @Component
接口 。
对,这也太麻烦了,不仅对于初学者来说麻烦,对于老手,也挺麻烦的。也许咱们的 Computer
没有那么麻烦,直接 new
出来不就行了。
这个时候就要介绍 @Inject
的另一种用法了,直接用于构造方法,这里我们修改一下依赖对象 Computer
类:
public class Computer {
private String name;
@Inject
public Computer() {
this.name = "自我构造的电脑";
}
//......
}
这里我们为 Computer
类添加一个默认构造方法,并添加了 @Inject
注解。那这个注解放到这里有什么用呢?它的作用就是当依赖注入器在所拥有的依赖供应商处查找依赖的提供方法时,如果找不到对应类型的依赖提供方法,那么 Dagger 就会去找这个依赖对象的类型有没有用 @Inject
声明的构造方法,如果有,那就通过这个构造方法生成这个依赖对象。
在例子中,就相当于买家要一台电脑,于是中通就去查找供应商,也就是淘宝或京东,但是中通找遍了发现这些供应商一个能给的都没有,都是垃圾,那怎么办呢?客户那边还等着要呢,没办法了只能去看看这个电脑有没有被 @Inject
标识的构造方法了,有的话直接 new 出来一个,就可以不用通过供应商了。
下面更改一下 TaoBao
这个供应商,先让它不能供应 Computer
:
@Module
public class TaoBao {
//......
// 现在淘宝这个供应商不再提供 Compupter
@Provides
public String getComputer() {
return "";
}
}
其他的不用动,这个时候再跑起来看一下输出:
System.out I 张三
System.out I 使用 自我构造的电脑 玩 赛博朋克2077
在这种情况下,没有供应商提供电脑,Dagger 就直接通过 Computer
中被 @Inject
标注的构造方法 new
出来一个对象给张三了。
是不是很简单?是的,确实比用 @Module
什么的简单多了。那我们为什么不都用这个方法呢?那是因为我们开发中肯定会用到别人的库,是无法修改别人的代码的,就更不用说在别人的构造方法上添加注解了,这个时候只能通过 @Module
这种依赖供应方来提供对象了。
依赖对象还依赖其他对象
上面的例子中,只是考虑到张三玩游戏是依赖于电脑的,但实际情况比这个复杂得多,因为,电脑其实还依赖于其他对象的,例如CPU、显卡之类的。在实际编码的时候,也很难见到这种一层依赖的情况,往往都是一个对象引用其他对象,其他对象又引用其他对象等等。在这种情况下,使用 Dagger 应该怎么整呢?
现在我们添加一个 CPU
类:
public class CPU {
private String producer;
public CPU(String producer) {
this.producer = producer;
}
@Override
public String toString() {
return producer + " CPU";
}
}
再修改一下 Computer
类,让其依赖于 CPU
这个类:
public class Computer {
private String name;
private CPU cpu;
@Inject
public Computer(CPU cpu) {
this.name = "自我构造的电脑";
this.cpu = cpu;
}
public Computer(String name, CPU cpu) {
this.name = name;
this.cpu = cpu;
}
public void play(String game) {
System.out.println("使用 " + name + "(" + cpu + ") 玩 " + game);
}
}
现在,CPU
作为一个依赖对象,它的创建方法放到哪里最好呢?回到上面的例子想一想,依赖对象是不是都由依赖提供方提供,那我们这里就把 CPU
的创建放到 @Module
标识的依赖提供方里,在这里,那就是 TaoBao
这个类了,毕竟淘宝上面是真的可以买到 CPU
的:
@Module
public class TaoBao {
//组装一台电脑
private Computer assembleComputer(CPU cpu) {
Computer computer = new Computer("淘宝组装的电脑", cpu);
return computer;
}
@Provides
public Computer getComputer(CPU cpu) {
return assembleComputer(cpu);
}
@Provides
public CPU getCPU() {
return new CPU("AMD");
}
}
我们先跑起来看一下,再讲解一下这个例子:
System.out I 张三
System.out I 使用 淘宝组装的电脑(AMD CPU) 玩 赛博朋克2077
这里看到,我们在 getComputer
这个 @Provides
方法添加了一个 CPU
类型的参数。这个时候 Dagger 在获取 Computer
时,会发现这个方法依赖于 CPU
类型的参数,这时它会去找提供 CPU
的方法,并先把 CPU
拿到,再交给提供 Computer
的方法,最终获取到一个 Computer
,完成对张三的依赖注入。这是一个递归的过程。
同理,如果被 @Inject
标识的构造函数要是依赖于其他的对象, Dagger 也会自动注入。也就是说,Dagger 提供了两种方式创建依赖对象:
- 调用被 @Inject 注解标识的构造方法
- 调用被 @Module 注解的类中提供相应的 @Provides 方法
而 Dagger 提供依赖流程可以概括为如下:
- 查找 @Module 类中是否存在创建该类的方法
- 如果存在,查看该方法是否存在参数 a. 存在参数,则按从步骤1 开始依次初始化每个参数 b. 不存在参数,则直接初始化该类实例,注入到依赖需求方
- 如果不存在,则查找该类中被 @Inject 标识的构造方法 a. 如果构造函数有参数,则按照从步骤1 开始依次初始化每个参数 b. 如果构造函数没有参数,则直接初始化该类实例,一次依赖注入到此结束
也就是说,Dagger 提供创建依赖对象的过程是递归的。
类型上再加限定:@Named 和 @Qulifier 注解的使用
通过上面的例子,我们已经学习了 Dagger 的基本的用法,张三也可以获取到电脑来玩游戏了。不过这个时候,张三吃着火锅唱着歌,玩着游戏喝着可乐,脑子里灵光一闪,觉得这个台式机玩游戏爽是爽,但是不能携带啊,自己这么高的段位好歹也要把电脑拿到星巴克给别人展示啊。简单来说,张三想把自己的台式机换成游戏本了。
那张三的这个需求咱们怎么满足,根据上面的说法,游戏本也能在淘宝上找到啊,那我们就直接修改 Taobao 这个依赖供应商,添加一个获取游戏本的方法:
@Module
public class TaoBao {
@Provides
public Computer getDesktop(CPU cpu) {
return new Computer("淘宝的台式机", cpu);
}
@Provides
public Computer getNotebook(CPU cpu) {
return new Computer("淘宝的笔记本", cpu);
}
//......
}
看似很完美,这下 Taobao
这个供应商既能供应台式机,又能供应笔记本了。现在编译一下代码:
demo/src/main/java/lic/swift/demo/dagger/ZTOExpress.java:6: 错误: [Dagger/DuplicateBindings] lic.swift.demo.dagger.Computer is bound multiple times:
public interface ZTOExpress {
^
@Provides lic.swift.demo.dagger.Computer lic.swift.demo.dagger.TaoBao.getComputer(lic.swift.demo.dagger.CPU)
@Provides lic.swift.demo.dagger.Computer lic.swift.demo.dagger.TaoBao.getNotebook(lic.swift.demo.dagger.CPU)
lic.swift.demo.dagger.Computer is injected at
lic.swift.demo.dagger.Person.computer
lic.swift.demo.dagger.Person is injected at
lic.swift.demo.dagger.ZTOExpress.deliverTo(lic.swift.demo.dagger.Person)
出错了,Dagger 显示 Computer
这个类被绑定了多次。为什么会出现这种情况呢?那是因为 Dagger 是通过函数的返回值类型来判断应该调用哪个方法获取依赖对象的,你这两个方法虽然函数名不一样,但是返回值类型都是一样的,在这种情况下 Dagger 需要获取一个 Computer
但它是不知道应该调用哪个方法来获取 Computer 的。
这个时候,就需要使用 @Named
注解了,这个注解使 Dagger 可以在返回值类型一样的情况下,再继续判断 @Named
注解的 value
值。现在继续修改 Taobao
这个供应商:
@Module
public class TaoBao {
@Provides
@Named("台式机")
public Computer getDesktop(CPU cpu) {
return new Computer("淘宝的台式机", cpu);
}
@Provides
@Named("笔记本")
public Computer getNotebook(CPU cpu) {
return new Computer("淘宝的笔记本", cpu);
}
//......
}
现在,通过 @Named
注解可以看到 getDesktop
返回的是 台式机 Computer
,getNotebook
返回的是 笔记本 Computer
。
依赖供应方这边搞定了,那作为依赖需求方的张三,肯定要说明一下,你要的是什么类型的电脑了,是台式机还是笔记本,那怎么说明呢,很简单,只需要在 @Inject
的成员上再加上 @Named
:
public class Person {
@Inject
@Named("笔记本") //告诉 Dagger 需要的是笔记本电脑
Computer computer;
//......
}
跑一下看看结果:
System.out I 张三
System.out I 使用 淘宝的笔记本(AMD CPU) 玩 赛博朋克2077
完美。现在张三可以拿着笔记本去星巴克打游戏了。而咱们也理解了 @Named
这个注解的用法了,简单来说,加上这个注解之后,Dagger 在判断类型时也会把这个注解带上进行判断。
那我们再想一想,为什么 @Named
这个注解这么牛呢?它是怎么实现的?
这个问题问得好!现在就该为大家介绍另外的一个注解 @Qulifier
。
qulifier
这个单词在英语中就是限定器的意思,顾名思义,在 Dagger 里肯定就是在类型相同时再进一步做个限定。它是一个元注解,@Named
就是继承于它。那我们怎么用这个注解呢?答案就是像 @Named
一样,自定义一个注解继承 @Qualifier
。
现在我们使用 @Qulifier
实现与上面相同的功能,先定义两个元注解分别表示台式机和笔记本:
@Qualifier
@Retention(RUNTIME)
public @interface DesktopComputer {
}
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface NotebookComputer {
}
然后,在代码中,替换上面使用 @Named
的地方。将 @Named("台式机")
替换为 @DesktopComputer
,将 @Named("笔记本") 替换为 @NotebookComputer
,其他地方不变。
在这么修改之后,你会发现代码跑起来的效果是一样的。这里就不啰嗦了。
另外还需要说明的是,像这种类型限定的注解 @Qulifier
,不仅仅可以标记在函数返回值和成员变量上,其实还可以标记在函数的参数上。这一点咱们后面再演示。
创建范围内的单例:@Singleton
和 @Scope
注解
我们在开发中,单例模式肯定是不少用的,但是大家可能不知道使用 Dagger 也能用来实现单例模式。这个特性得益于 Dagger 中范围的这个概念,也就是 @Scope
注解。这里说一下 Dagger 这个范围是啥意思,再演示怎么使用。简单来说,就是 Dagger 可以定义一个某某范围,在这个某某范围内,不会创建多个依赖对象,而是仅创建一个。
在上面的例子中,张三可以在外面用笔记本电脑玩游戏,也可以在家里用台式机玩游戏,但是这有一个问题。现在游戏那么大,动不动就几十个G,在家里还好说,那出门了再下载个游戏岂不是费死劲。流量贵不说,那网速是自从搞5G之后就下降了不少,这要把游戏下载下来,等到星巴克打烊也难说。那怎么整呢?硬盘啊,4G之前不就是通过移动硬盘来拷贝游戏的么。这里张三把游戏装到移动硬盘里不就行了,走到哪儿都可以用。
这里我们假设电脑都是依赖于这个硬盘,那么无论是台式机还是笔记本,都应该是依赖于同一个硬盘,如果不是,那就是张三把游戏安装错了,安装到系统盘里了。
咱们先给 Computer
类添加一个 Hardware
的依赖对象:
public class HardDisk {
private String name;
public HardDisk(String name) {
this.name = name;
}
@Override
public String toString() { //这里我们打印了地址
return name + " 硬盘@" + Integer.toHexString(hashCode());
}
}
修改一下 Computer
类:
public class Computer {
private String name;
private CPU cpu;
private HardDisk hardDisk; //电脑的硬盘
public Computer(String name, CPU cpu, HardDisk hardDisk) {
this.name = name;
this.cpu = cpu;
this.hardDisk = hardDisk;
}
public void play(String game) {
System.out.println("使用 " + name + "(" + cpu + "," + hardDisk + ") 玩 " + game);
}
}
现在 Computer
是依赖需求方,已经搞定了,在做完这些之后,那就要处理依赖供应方,依赖供应方是谁呢,那肯定是淘宝啊:
@Module
public class TaoBao {
@Provides
public CPU getCPU() {
return new CPU("AMD");
}
@Provides
public HardDisk getHardDisk() {
return new HardDisk("希捷");
}
@Provides
@DesktopComputer
public Computer getDesktop(CPU cpu, HardDisk hardDisk) {
return new Computer("淘宝的台式机", cpu, hardDisk);
}
@Provides
@NotebookComputer
public Computer getNotebook(CPU cpu, HardDisk hardDisk) {
return new Computer("淘宝的笔记本", cpu, hardDisk);
}
}
依赖提供方也搞定了,那依赖注入器需要修改吗?这个问题是值得想一想的。
答案是不需要,如果需要的话,就体现不出来 Dagger 的优势了。前面也说过,Dagger 在创建依赖对象的时候,是一个递归的过程。在这里 Dagger 只是为 Person
进行依赖注入,但是在注入 Computer
的时候发现创建 Computer
还需要 CPU
,那 Dagger 就先去创建 CPU
这个依赖对象,然后发现还需要 HardDisk
对象,那就再去找,两个都找到了,才能创建出来一个 Computer
,然后将这个 Computer
注入给 Person
对象。因此依赖注入器是不需要修改的。
但是我们这里修改一下 Person
让这个类在玩游戏时能够显示持有的电脑是否使用相同的硬盘:
public class Person {
@Inject
@DesktopComputer
Computer desktop;
@Inject
@NotebookComputer
Computer notebook;
public void playGame(String gameName) {
System.out.print(name + "\n");
desktop.play("\t" + gameName);
notebook.play("\t" + gameName);
}
//......
}
这里我们想前面使用 Computer
一样使用 HardDisk
,这个时候应该用的不是同一个硬盘:
System.out I 张三
System.out I 使用 淘宝的台式机(AMD CPU,希捷 硬盘 @504f62) 玩 赛博朋克2077
System.out I 使用 淘宝的笔记本(AMD CPU,希捷 硬盘 @22dd8f3) 玩 赛博朋克2077
可以看到两个硬盘的内存地址是不同的,这是两个不同的硬盘。 那怎么让两台电脑使用相同的硬盘呢?最简单的方式就是添加 @Singleton
注解。
首先就是需要在依赖供应方的相关方法上添加这个注解,这里就是 Taobao.getHardDisk()
方法:
@Provides
@Singleton
public HardDisk getHardDisk() {
return new HardDisk("希捷");
}
在没有添加这个注解之前,当注入器发现需要这个类型的依赖,就会调用一次这个方法,这会创建一个全新的对象。就像我们到淘宝商城上买东西一样,买回来的当然是新的。但是添加了这个注解后,就相当于淘宝这个依赖供应方说明了,这个类型的对象,在这个范围里只有一个,谁来用就拿给谁,都是同一个。在这个例子中,就可以想象为这个硬盘是张三专属的范围,张三只有一个硬盘,只能获取到这一个。
其次,还需要在依赖注入器上添加这个注解:
@Singleton
@Component(modules = {TaoBao.class})
public interface ZTOExpress {
void deliverTo(Person person);
}
为什么需要在依赖注入器上也添加这个范围呢?大家可以这么理解,这个中通可以配送淘宝上的任何东西(@Provides
方法),但现在淘宝上有个专属于张三的东西,你既然说都指定了供应方为淘宝(modules = {TaoBao.class}
),那是不是淘宝上的所有范围的东西都能配送。既然如此你也得声明一下,以表示你可以配送这个范围内的东西。要不默认情况下,大家都会觉得你不能进行特殊范围的物品的配送的。
而在代码层面,这个注解的意义就在于:Dagger 在同一个作用范围内,@Provide
方法提供的依赖对象就会变成单例,也就是说依赖需求方不管依赖几次 @Provide
方法提供的依赖对象,Dagger 都只会调用一次这个方法。
下面我们跑起来,看看结果:
System.out I 张三
System.out I 使用 淘宝的台式机(AMD CPU,希捷 硬盘 @504f62) 玩 赛博朋克2077
System.out I 使用 淘宝的笔记本(AMD CPU,希捷 硬盘 @504f62) 玩 赛博朋克2077
可以看到是使用的相同的硬盘。这就是 @Singleton
注解的作用。在这里就表示,通过中通从淘宝上拿到的硬盘都是这一块。但是这样也不太对,中通肯定不止为张三配送,那它为李四配送的时候,岂不是也送的张三的硬盘?
所以这时候就别用自带的 @Singleton
范围,而是自定义一个范围,也就是使用 @Scope
注解。现在我们就为张三创建一个专属的范围,通过这个例子咱们也会明白 @Scope
的使用了:
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface SanScope {
}
这里使用了 @Scope
这个元注解创建了张三专属范围,现在我们用这个注解替换前面的 @Singleton
注解。你会发现结果是一样的,这里就展示了。
在一般 Android 开发中,往往会创建诸如 @PerActivity
或 @PerFragment
这样的范围注解。例如某些个 Activity
中的依赖的对象应该是相同的,即会用到 @PerActivity
。
在使用 @Scope
范围注解时,一定要注意两点:
- 如果是通过依赖对象的构造函数创建依赖时,需要在类名上添加范围注解,不能在构造函数上添加,否则无效。
- 范围内单例的前提是使用了相同的依赖注入器。
在上一个文章中,我们介绍了 Dagger 中的限定和范围注解,现在我们将视线转移到依赖注入器来,先介绍这个组件的依赖的两种方式,再介绍两个常用的类型。
让依赖注入器依赖其他依赖注入器(@Component中的 dependencies 属性)
在实际生活中,一个供应商基本是无法完成所有物品的供应的,但是为了保证商品的尽可能完整,它们可能会依赖其他的供应商。例如在淘宝中,它能供应很多东西,但是有些东西它还是需要找其他供应商的。例如一台电脑中的 CPU,这玩意全球也就两家能生产,所以就是依赖于AMD 和 Intel,有人买的时候,淘宝也是找这两家店拿货。
在本文的例子中,张三从 Taobao
买电脑,其实 CPU 也是要从其他供应商拿的。现在我们就通过这个例子来修改一下我们的代码。
但是在修改之前我们得先思考这样的一个问题,在面向对象的思想中,针对这样的场景,我们往往会抽取父类,使用继承来做,使淘宝这个供应商继承 CPU 供应商
,这样淘宝就有了提供 CPU 的能力了。
不过这是在我们普通编程中,在 Dagger 中,就必须使用注入器的依赖,即 @Component
的 dependencies
属性。我们先将 getCPU
的方法从 Taobao
抽离出来,放到一个 CPUProvider
中:
@Module
public class CPUProvider {
@Provides
@Named("AMD")
public CPU getAMDCPU() {
return new CPU("AMD");
}
@Provides
@Named("Intel")
public CPU getIntelCPU() {
return new CPU("Intel");
}
}
CPU 供应商能够提供两种类型的 CPU:AMD 的和 Intel 的,但要想从供应商拿货,还需要一个依赖注入器,类似于中通快递。由于 CPU 也就美国这两个公司能生产,那这个专门用于配送 CPU 的注入器我们就叫他 UPS 吧:
@Component(modules = CPUProvider.class)
public interface UPSExpress {
@Named("AMD") CPU getAMDCPU();
@Named("Intel") CPU getIntelCPU();
}
注意,对于一般的依赖注入器,我们会定义类似于 injectTo
这样的函数。但是这个依赖注入器是给其他依赖注入器使用的,因此不需要 inject
方法。定义的方法的返回值需要是这个注入器能够提供的类型,否则依赖它的依赖注入器就无法找到这个类型。现在还需要修改一下 ZTOExpress
这个接口,为其添加 dependencies
这个方法:
@SanScope
@Component(modules = {TaoBao.class}, dependencies = UPSExpress.class)
public interface ZTOExpress {
void deliverTo(Person person);
}
好了,现在咱们就可以使用 ZTOExpress
对 Person
对象进行依赖注入了。也许你不太懂为什么会有依赖注入器还需要依赖另一个依赖注入器这种情况,可以联想一下跨境电商的配送的过程,一个东西从国外的商店买过来首先要经过国外的快递公司,到了海关,再由国内快递公司接手。这不是就通了。
在进行依赖注入时,还需要修改一点代码:
Person person = new Person("张三");
UPSExpress upsExpress = DaggerUPSExpress.builder().cPUProvider(new CPUProvider()).build();
ZTOExpress ztoExpress = DaggerZTOExpress.builder().taoBao(new TaoBao()).uPSExpress(upsExpress).build();
ztoExpress.deliverTo(person);
person.playGame("赛博朋克2077");
这里创建了 UPSExpress
的实例,并且将其传入到 ZTOExpress
实例中,然后 ZTOExpress
还能够完成注入。
另外,看一看 Dagger 生成的这函数的名字,cPUProvider
、uPSExpress
,真是不忍直视,吐槽一下。最后,看下运行结果:
System.out I 张三
System.out I 使用 淘宝的台式机(AMD CPU,希捷 硬盘 @86589c2) 玩 赛博朋克2077
System.out I 使用 淘宝的笔记本(Intel CPU,希捷 硬盘 @86589c2) 玩 赛博朋克2077
最后提一下,在我们正常的开发过程中,像 UPSExpress
这种其他具体注入器可能会依赖的注入器,一般都会提前在某个地方以单例的方式创建好,当要创建具体注入器时候,再将其设置到具体的注入器中。例如,在 Application
类的 onCreate
方法中创建并提供 get
方法。
使用 @SubComponent
定义子组件、即子依赖注入器
对于上面的场景,中通能够完成对张三的配送,但是在现实生活中,中通这么大的公司,在全国是有很多分部的,真实的配送任务是会被分配给这些地区部门的。一个部门大了,就会有子部门,放到依赖注入器中,如果这个依赖注入器太复杂,那就应该划分为若干个子的依赖注入器,这就要用到 @SubComponent
这个注解了。
我们现在假设张三是住在大上海的,那么为张三进行配送的,肯定是中通的上海分部。咱们就先定义一个中通的上海分部:
@Subcomponent
public interface ZTOShanghaiExpress {
void deliverTo(Person person);
}
这个上海分部是属于中通的,那么在使用的时候,我们肯定是通过本部才能拿到分部的实例再使用的,也就是说,在 ZTOExpress
中应该有一个返回上海分部的方法:
@SanScope
@Component(modules = {TaoBao.class}, dependencies = UPSExpress.class)
public interface ZTOExpress {
void deliverTo(Person person);
ZTOShanghaiExpress getShanghaiDepartment();
}
在使用的时候,我们就通过 getShanghaiDepartment
这个方法返回的 ZTOShanghaiExpress
来进行注入,由于不用总部进行配送,因此 ZTOExpress.deliverTo
这个方法其实是可以删除的。
注入的代码如下:
Person person = new Person("张三");
UPSExpress upsExpress = DaggerUPSExpress.builder().cPUProvider(new CPUProvider()).build();
ZTOExpress ztoExpress = DaggerZTOExpress.builder().taoBao(new TaoBao()).uPSExpress(upsExpress).build();
ZTOShanghaiExpress ztoShanghaiExpress = ztoExpress.getShanghaiDepartment(); //重点
ztoShanghaiExpress.deliverTo(person);
person.playGame("赛博朋克2077");
这里可以看到是通过 ZTOExpress
的 getShanghaiDepartment
获取到一个 ZTOShanghaiExpress
实例,然后再惊醒注入操作。
这里与 @Component
不同的是,这个 ZTOShanghaiExpress
没有为其设置 modules
属性,也没有设置 @Scope
注解,那它是怎么通过编译的呢?这其实是因为当它属于某个依赖注入器时,会继承其父组件的注解。此处,ZTOShanghaiExpress
就继承了 ZTOExpress
的 @SanScope
和 @Component(modules = {TaoBao.class}, dependencies = UPSExpress.class)
。
延迟加载 Lazy
和 强制重新加载 Provider
在 Dagger 中, Lazy
和 Provider
都是用于包装需要被 Dagger 注入的类型,这就有点像 Java 中的 WeakReference
。其中 Lazy
用于延迟加载,所谓的懒加载就是当你需要用到该依赖对象时,Dagger 才帮你去获取一个;Provide
用于强制重新加载,也就是每一要用到依赖对象时,Dagger 都会帮你依赖注入一次。
这里我们修改一下 Person
对象,使其 computer
用 Lazy
包装,用到时再初始化;另外再添加一个 cola
的对象,每次用到时,都应该返回一瓶新的可乐:
public class Person {
@Inject
@DesktopComputer
Lazy<Computer> desktop;
@Inject
@NotebookComputer
Lazy<Computer> notebook;
@Inject
Provider<Cola> cola;
public void playGame(String gameName) {
System.out.print(name + "\n");
desktop.get().play("\t" + gameName);
notebook.get().play("\t" + gameName);
System.out.println("\t 喝了一瓶可乐:"+cola.get());
System.out.println("\t 再了一瓶可乐:"+cola.get());
System.out.println("\t 还了一瓶可乐:"+cola.get());
}
//......
}
然后我们让 TaoBao
这个依赖供应商提供能够提供 Cola
,一个普通的 @Provides
方法:
@Module
public class TaoBao {
@Provides
public Cola getCola() {
System.out.println("TaoBao 获取一瓶可乐");
return new Cola();
}
//......
}
其他的地方不用更改,我们重新跑一下:
System.out I 张三
System.out I TaoBao 创建台式机
System.out I 使用 淘宝的台式机(AMD CPU,希捷 硬盘 @bb30609) 玩 赛博朋克2077
System.out I TaoBao 创建笔记本
System.out I 使用 淘宝的笔记本(Intel CPU,希捷 硬盘 @bb30609) 玩 赛博朋克2077
System.out I TaoBao 获取一瓶可乐
System.out I 喝了一瓶可乐:lic.swift.demo.dagger.Cola@b06800e
System.out I TaoBao 获取一瓶可乐
System.out I 再了一瓶可乐:lic.swift.demo.dagger.Cola@59cfa2f
System.out I TaoBao 获取一瓶可乐
System.out I 还了一瓶可乐:lic.swift.demo.dagger.Cola@76b083c
可见 compute
和 cola
都是使用时才去调用的,只是 getCola
调用了多次。
如果你使用 Dagger 的话,那么这两个类型肯定会比较常用,不过还好这两个类型都不难,使用过 WeakReference
就知道这两个类型也就是相同的用法。
在前面的例子中,我们吐槽了在默认情况下,Dagger 生成的代码会有一些方法名不怎么好看,例如上面的 cPUProvider
、uPSExpress
这种。那么有人就说了,这个地方能不能改得更好看一点呢?答案肯定是可以的。
这就要介绍 @Component
注解中的另外两个注解了: @Component.Builder
和 @Subcomponent.Factory
。通过这两个注解,我们可以对 Dagger 生成 Component
的实现类有一定的控制。
当我们创建一个类时,常用的设计模式就是 Builder 构造器模式和 Factory 工厂模式,其实这两个就对应了上述的两个注解。这里我们一个一个说,先说更为常用的 @Component.Builder
,然后再介绍 @Subcomponent.Factory
。
@Component.Builder
在我们不往 Component
接口中添加 Builder
或 Factory
时,Dagger 模式使用的就是这种构造器模式,但是咱们仍然可以使用这个注解,对生成代码的过程进行一些控制。
这里我们就修改一下 ZTOExpress
这个类,为其添加一个 Builder
:
@SanScope
@Component(modules = {TaoBao.class}, dependencies = UPSExpress.class)
public interface ZTOExpress {
void deliverTo(Person person);
ZTOShanghaiExpress.Factory getShanghaiDepartmentFactory();
@Component.Builder
interface Builder {
Builder setTaoBao(TaoBao taoBao);
Builder setUPSExpress(UPSExpress upsExpress);
ZTOExpress build();
}
}
很简单,在 ZTOExpress
中再添加一个 Builder
并使用 @Component.Builder
标注,而 Builder
里面的内容,就是很普通的构造器模式,咱们可以在里面修改代码。
在添加了这段代码后,只需要调用我们自己定义的 set
方法构造 ZTOExpress
然后再使用就行。这里就不演示了。
@Component.Factory
除了使用 Builder
模式,Dagger 还支持 Factory
工厂模式,其使用方式也跟上面的 Builder
类似。但是有一点需要注意的是,同一个 Component
是不能即添加 Builder
也添加 Factory 的。
由于上面已经给 ZTOExpress
添加了 Builder
,那我们现在就给 UPSExpress
添加 Factory
注解:
@Component(modules = CPUProvider.class)
public interface UPSExpress {
@Named("AMD") CPU getAMDCPU();
@Named("Intel") CPU getIntelCPU();
@Component.Factory
interface Factory {
UPSExpress create(CPUProvider cpuProvider);
}
}
在使用了工厂模式之后,我们再构造 UPSExpress
就得用如下的方式:
UPSExpress upsExpress = DaggerUPSExpress.factory().create(new CPUProvider());
@Subcomponent.Builder 和 @Subcomponent.Factory
不仅仅是依赖注入器可以添加构造器模式或工厂模式,子组件也可以使用构造器模式或工厂模式。只不是使用的是 @Subcomponent.Builder
和 @Subcomponent.Factory
。
例如,我们给中通的上海分部添加一个 Factory
:
@Subcomponent
public interface ZTOShanghaiExpress {
void deliverTo(Person person);
@Subcomponent.Factory
interface Factory {
ZTOShanghaiExpress create();
}
}
在使用时,由于子组件是要放到主组件中的,在这么写之后,就需要修改获取子组件的方式:
ZTOShanghaiExpress.Factory getShanghaiDepartmentFactory();
如果是添加了 @Subcomponent.Builder,那就改成下面的这样:
ZTOShanghaiExpress.Builder getShanghaiDepartmentFactory();
总结
Dagger 是一个优秀的依赖注入框架,特别是在 Android 开发中,有大量的项目在使用。本文用了大量的篇幅来讲解这个库,但是也只是涉及到了这个库的一小部分而已,仍然有大量的注解还没有讲到,例如 @Binds
、@BindsInstance
、@IntoMap
、@ClassKey
、@AssistedInject
、@Assisted
、@AssistedFactory
、@StringKey
等等。
Dagger 库内容庞大,若是想要全部弄明白这个库的所有功能,光凭一篇文章是不可能的。但是作为基本使用,本篇文章是值得一看的。在了解了本文内容之后,再碰到 Dagger 中的其他注解,学起来也不会很困难。
最后,祝各位大佬身体健康,升职加薪,愿我们的技术人生没有35岁危机。
转载自:https://juejin.cn/post/7392413389278707748