这可能是本年度最好用的 Dagger 使用教程 二(限定注解 @Named、@Qulifier 和 范围注解 @Singleton、@Scope)

前言

Dagger 是一个优秀的依赖注入框架,特别是在 Android 开发中,有大量的项目在使用。本文用了一个形象的例子,用大量的篇幅来讲解这个库,因为文章过长,因此分为四个部分,请大家按照顺序来看,本文是这个系列的第二章:

  1. 这可能是本年度最好用的 Dagger 使用教程 一(基本使用)
  2. 这可能是本年度最好用的 Dagger 使用教程 二(限定注解 @Named、@Qulifier 和 范围注解 @Singleton、@Scope)
  3. 这可能是本年度最好用的 Dagger 使用教程 三(依赖注入器的依赖、子组件、Lazy、Provider)
  4. 这可能是本年度最好用的 Dagger 使用教程 四(使用 @Builder 和 @Factory 创建依赖注入器)

另外,这个是我的微信公众号,也是最近在做,希望大家能够多多关注,我会不定期更新优秀的技术文章:

接下来,开始我们的正文吧:

类型上再加限定:@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 返回的是 台式机 ComputergetNotebook 返回的是 笔记本 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 范围注解时,一定要注意两点:

  • 如果是通过依赖对象的构造函数创建依赖时,需要在类名上添加范围注解,不能在构造函数上添加,否则无效。
  • 范围内单例的前提是使用了相同的依赖注入器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

李斯维

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值