通过上一篇文章我们知道了 Dagger2 的基本使用,在这篇文章中,我们将讲解 Dagger 中的两个重要概念以及相关注解。
这可能是最详细的 Dagger2 使用教程 一(基本使用)
类型上再加限定:@Named 和 @Qulifier 注解的使用
通过上面的例子,我们已经学习了 Dagger 的基本的用法,张三也可以获取到电脑来玩游戏了。不过这个时候,张三吃着火锅唱着歌,玩着游戏喝着可乐,脑子里灵光一闪,觉得这个台式机玩游戏爽是爽,但是不能携带啊,自己这么高的段位好歹也要把电脑拿到星巴克给别人展示啊。简单来说,张三想把自己的台式机换成游戏本了。
那张三的这个需求咱们怎么满足,根据上面的说法,游戏本也能在淘宝上找到啊,那我们就直接修改 Taobao 这个依赖供应商,添加一个获取游戏本的方法:
@Module
public class TaoBao {@Providespublic Computer getDesktop(CPU cpu) {return new Computer("淘宝的台式机", cpu);}@Providespublic 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 atlic.swift.demo.dagger.Person.computerlic.swift.demo.dagger.Person is injected atlic.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;}@Overridepublic 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 {@Providespublic CPU getCPU() {return new CPU("AMD");}@Providespublic HardDisk getHardDisk() {return new HardDisk("希捷");}@Provides@DesktopComputerpublic Computer getDesktop(CPU cpu, HardDisk hardDisk) {return new Computer("淘宝的台式机", cpu, hardDisk);}@Provides@NotebookComputerpublic 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@DesktopComputerComputer desktop;@Inject@NotebookComputerComputer 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
范围注解时,一定要注意两点:
- 如果是通过依赖对象的构造函数创建依赖时,需要在类名上添加范围注解,不能在构造函数上添加,否则无效。
- 范围内单例的前提是使用了相同的依赖注入器。