[11]——MVP 模式之 先说说 Dagger (2)

转载请标明原文地址:http://www.jianshu.com/p/dc163215bc7e


本来打算继续写 MVP 模式的,但是看了网上的几篇 Dagger 介绍的文章后,还是决定先写写 Dagger,网上有些文章写的不是过于简单就是太过复杂,或是不够详实,让刚接触 Dagger 的人容易看的云里雾里。正好也是刚学习 Dagger 没多久,记录下来对自己也是一个查缺补漏。文中如有错误,请各位大佬予以斧正!

本文示例代码:https://github.com/junerver/DaggerDemo


Dagger2

注:本文中的 Dagger 都是指 Google 推出的 Dagger2

Dagger 是 Square 公司推出的一个 DI(依赖注入)框架,后来项目被 Google 接手,大家习惯性称之为 Dagger2。

依赖注入:可能有的朋友看到依赖注入这四个字就迷惑了,这是什么gui?那你听听控制反转呢?是不是更难懂,更加拗口。其实在我们的程序中存在着大量的依赖,这里的依赖不是指我们的项目依赖第三方库,而是指我们的对象依赖于其他的实例。只要实例 A 中用到了 B 的实例,我们就称之为 A 依赖于 B。比如StringBuffer stringBuffer = new StringBuffer("hello world");这里的 StringBuffer 类就依赖于 String 类。

在 J2EE 领域依赖注入使用很普及,对于大型项目而言存在着大量的实例,这些实例之间互相依赖,为了方便调用者使用,依赖注入顺势而生,比如 Spring 框架中就包含了依赖注入的功能。

在 Android 中依赖注入起步较晚,其原因大概是因为早期的 Android 工程普遍不大,而现在的 Android 工程动辄上百近千个页面,已经可以视为大工程来看待的了,所以依赖注入框架也开始渐渐流行起来(同理像一些ORM框架也是这样的);

酿酒大师教你酿酒

上面我们提到了为什么依赖注入开始流行起来,我们来看看不使用依赖注入的一个示例代码吧:

//制作白兰地的流程
new Brandy(new Distiller(), new Wine(new Grape("解百纳"), new FermentBarrel()));

看了这段代码我估计你要骂街了,这是什么鬼!?不是说要介绍依赖注入的优势么,这一大堆的 new 是什么东西?

别急别急,没看我的注释是“制作白兰地的流程”么,我这是在制作一款用解百纳酿造的白兰地啊~,你看我们想要喝白兰地需要先有酒液原浆吧,需要有蒸馏器吧,把原浆蒸馏了才能得到白兰地嘛。而酒液原浆的获得又需要用到葡萄和发酵桶嘛。

其实上面这段代码我们就是模拟了一个相对复杂的实例化过程,可以看到其中的依赖关系如下

    |--- 蒸馏器
白兰地  
    |       |--- 葡萄
    |--- 原浆
            |--- 发酵桶

那么在每次我们视图实例化“白兰地”对象时,都需要首先将其他的四个对象先行实例化完毕,实例化一次还好,如果这个实例在上百个页面中都需要使用到呢?但如果我们要是使用了依赖注入框架,将会使这一步骤变得十分容易。

@Inject
Brandy mBrandy;
...
...
@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    ...
    ...
    mInjection.inject(this);
}

是的,我们只需要在申明对象时,加上一行注解即可。然后在需要使用的地方直接使用mBrandy,是不是方便了很多。

如何使用

添加依赖

看完上面的代码对比,相比你已经对 Dagger 产生了强烈的兴趣了吧。如此优雅、高效的方式怎么会不吸引人呢,下面我们来看看如何在项目中使用 Dagger 。

注意:我使用的环境是 AS 2.0,使用的 Dagger 版本是 2.4。如果你是使用 AS 2.2,请参照Dagger2官方的说明。

首先我们需要在项目中加入 Dagger 的依赖,首先在项目的build.gradle文件中添加classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.0.0'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'// <----添加这一句
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

然后在 app 模块下的build.gradle文件中添加apply plugin: 'com.neenbedankt.android-apt'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:25.0.1'

    //引入dagger2
    compile 'com.google.dagger:dagger:2.4'
    apt 'com.google.dagger:dagger-compiler:2.4'
    //java注解
    provided 'org.glassfish:javax.annotation:10.0-b28'
}

然后选择 Sync Now即可;

了解几个小概念

  • @Inject
    @Inject 有两个功能:
    1、注解类的构造方法,其作用可以理解为通过注解将这个类的实例化方法告诉 Dagger;
public class Distiller {
    @Inject
    public Distiller() {
    }
    @Override
    public String toString() {
        return "蒸馏器";
    }
}

2、在需要使用该实例的地方注解,其作用是告诉调用者,这个被注解的实例由 Dagger 来负责实例化;

@Inject
Distiller mDistiller;
  • @Module
    @Module 可以理解为一个生产实例的工厂,他掌握各个需要注入的类的实例化方法,当 Dagger 需要为某个类注入实例时,会到 @Module 注解的类中,查找这个类的实例化方法。当然这一过程是需要通过使用 @Provides 注解的有返回值的方法,来告知 Dagger 的。
@Module
public class BrandyModule {
    @Provides
    public Grape provideGrape() {
        return new Grape("解百纳");
    }
}
  • @Component
    @Component 用来注解一个接口,在编译的时候会生成 Dagger+文件名 的新Java文件。Component可以理解为注射器,它是连接被注入的类与需要被注入的类之间的桥梁。
@Component(modules = BrandyModule.class)    //这里的modules参数可以是多个,用于告诉“注射器”都有哪些实例可以被注射到目标类中
public interface BrandyComponent {
    void inject(MainActivity mainActivity);
}
  • @Provides
    在提供实例的方法上注解,用于告诉 Dagger 这是一个用于注入的实例。方法名可以随便,Dagger 是通过方法的返回值来将其添加到依赖列表的。
@Provides
public Grape provideGrape() {
    return new Grape("解百纳");
}

看个小例子

如果你按照上面的步骤写下了代码,运行代码你会发现报了一个空指针错误,这是因为我们还没有为我们要注入的对象与被注入的类建立联系。我们需要在使用被注入实例之前调用如下方法DaggerBrandyComponent.create().inject(this);,注意这里的参数 this ,这个参数的值由我们刚刚定义的接口 BrandyComponent 中声明的方法的参数决定。为了方便测试,我新建了一个 TestDagger 类,并在接口中添加了一个方法void inject(TestDagger testDagger);

public class TestDagger {
    @Inject
    Distiller mDistiller;

    public TestDagger() {
        DaggerBrandyComponent.create().inject(this);    //在代码中我们并没有对 Distiller 对象进行 new 操作来实例化
    }

    @Override
    public String toString() {
        return mDistiller.toString();
    }
}

这样做是为了可以方便的在测试用例中进行测试,而不用将程序运行在手机或者模拟器上。


使用测试用例来方便的测试

可以看到,下面输出了“蒸馏器”,这说明我们对这一实例的注入成功了。

不得不说的Module

Interesting

看了上面的例子,你是不是觉得有点意思了?但是要注意的是,上面的那种方法适用于无参的构造方法(当然也可以有参数,但是对应的参数的构造方法上也要有 @Inject 注解)。Talk is cheap,show me the code!

为了验证刚刚提到的那一点,我们来为蒸馏器的构造方法添加一个新的参数 Heater 加热器!

public class Distiller {

    private Heater mHeater;
    
    @Inject
    public Distiller(Heater heater) {
        mHeater = heater;
    }

    @Override
    public String toString() {
        return "蒸馏器";
    }
}

public class Heater {
    
    public Heater() {
    }

    @Override
    public String toString() {
        return "加热器";
    }
}

注意上面的代码,Heater 类的构造方法没有使用 @Inject 注解,我们运行一下看看效果。


Heater类的实例无法提供

程序抛出错误,错误的内容是:在没有使用 @Inject 注解构造方法或者 @Provides 注解一个方法时无法提供 Heater 的实例。我们只需要在 Heater 类的构造方法上也加上 @Inject 注解就可以了。

通过上面的例子,你应该了解了我们可以为构造方法参数的构造方法添加 @Inject 注解来实现注入。(绕口令:八百标兵奔北坡,北坡炮兵并排跑,炮兵怕把标兵碰,标兵怕碰炮兵炮)。

但是如果我们的参数是第三方的类呢?比如参数是一个 String 呢?我们不可能去 String 类的构造方法中添加注解。这时候就需要用到 Moudle 类了。

Module 的代码上面我们已经写过了,我们来看一下

@Module
public class BrandyModule {

    @Provides
    public Grape provideGrape() {
        return new Grape("解百纳");
    }
}

我们的 Grape 葡萄类的构造方法有一个 String 参数,通过 @Provides 注解,我们可以告知 Dagger 当需要用到 Grape 类的实例的时候,来 Module 类中获取。再次运行代码查看结果:


运行结果

可以看到我们输出了正确的结果:我们来梳理一下 Dagger 注入实例的过程:

  • 步骤1:查找Module中是否存在创建该类的方法。
  • 步骤2:若存在创建类方法,查看该方法是否存在参数
    • 步骤2.1:若存在参数,则按从步骤1开始依次初始化每个参数
    • 步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
  • 步骤3:若不存在创建类方法,则查找Inject注解的构造函数,看构造函数是否存在参数
    • 步骤3.1:若存在参数,则从步骤1开始依次初始化每个参数
    • 步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束

:同时存在 @Inject 与 Module 时,Module 的优先级高于 @Inject 注解。

本文示例代码:https://github.com/junerver/DaggerDemo

酒鬼总是希望可以多喝几种酒

现在你应该理解了 Dagger 是怎么一个工作流程了吧!也许你会问了,纪然 Dagger 是通过被注解方法的返回值来将它添加到依赖列表的,那么我们如果有多个 Grape 实例可用,应该怎么办呢(1、如何为 Dagger 创建多个相同类的的实例;2、在需要注入时如何区分多个实例;)?

首先我们再添加一个 @Provides 注解方法试试看:


bound multiple times

可以看到,编译器报错,提示 bound multiple times(多次绑定),难道说 Dagger 只能注入一种实例么??那他的局限性也太大了吧?这时候 @Named 注解就需要粉墨登场啦~

@Named 注解用于给 @Provides 注解提供别名,在使用的时候也需要加上 @Named 注解,Dagger 就知道我们需要的是具体哪个实例了。

@Provides
@Named("CabernetSauvignon")
public Grape provideOtherGrape() {
    return new Grape("赤霞珠");
}
默认的注入实例
使用别名的注入实例

可以看到我们现在可以通过 @Named 注解来活动不同的葡萄了,那需要使用 Wine 类如果希望使用赤霞珠作为参数应该怎么办呢,如下所示:

@Provides
@Named("CabernetSauvignon")
public Wine provideOtherWine(@Named("CabernetSauvignon") Grape grape, FermentBarrel fermentBarrel) {
    return new Wine(grape, fermentBarrel);
}

我们新增加一个提供赤霞珠原浆的方法,在其参数中使用了@Named("CabernetSauvignon")来指定,这个参数是赤霞珠。完整的代码如下所示:

@Module
public class BrandyModule {

    @Provides
    public Grape provideGrape() {
        return new Grape("解百纳");
    }

    @Provides
    @Named("CabernetSauvignon")
    public Grape provideOtherGrape() {
        return new Grape("赤霞珠");
    }

    @Provides
    @Named("CabernetSauvignon")
    public Wine provideOtherWine(@Named("CabernetSauvignon") Grape grape, FermentBarrel fermentBarrel) {
        return new Wine(grape, fermentBarrel);
    }

    @Provides
    @Named("CabernetSauvignon")
    public Brandy provideOtherBrandy(@Named("CabernetSauvignon") Wine wine, Distiller distiller) {
        return new Brandy(distiller, wine);
    }
}

现在我们就可以品尝到使用赤霞珠葡萄制作的白兰地啦~

品尝美酒吧!

总结要点:

  1. @Inject 有两种用途;
  2. 对于不能使用 @Inject 注解的类,将该类的实例化方法使用 @Provides 注解;
  3. 对于同一个类的不同实例化方法,使用 @Named 注解;
  4. @Named 注解还可以注解 Provides 方法的参数;

我们不需要那么多的蒸馏器

我们将 Distiller 的 toString() 方法进行修改:

public class Distiller {

    private Heater mHeater;

    @Inject
    public Distiller(Heater heater) {
        mHeater = heater;
    }

    @Override
    public String toString() {
        return "有"+mHeater.toString()+"的蒸馏器"+super.toString();
    }
}

再次运行程序:


使用了不同的蒸馏器

发现问题了吗?我们的白兰地居然使用了不同的蒸馏器,这很不合理,我们的酿酒作坊只需要一个蒸馏器就可以了,完全不需要对每瓶酒都使用一个新的蒸馏器。也就是说我们的 Distiller 类应该是一个单例!

如果你看过其他文章你应该会知道有一个注解 @Singleton,这个注解的字面就是单例,那么我们使用该注解来注释我们的 Distiller 类以及我们的 BrandyComponent 接口。再次运行程序,结果如下:

@Singleton
public class Distiller {

    private Heater mHeater;

    @Inject
    public Distiller(Heater heater) {
        mHeater = heater;
    }

    @Override
    public String toString() {
        return "有"+mHeater.toString()+"的蒸馏器"+super.toString();
    }
}

@Component(modules = BrandyModule.class)
@Singleton
public interface BrandyComponent {
    void inject(MainActivity mainActivity);
    void inject(TestDagger testDagger);
}
现在使用的是相同的蒸馏器了

你可能会惊叹:我的天呐!真是魔法!!!我们居然通过一个注解就实现了单例模式!?还学什么7种单例模式的实现方式啊,以后都用这个注解不就都搞定了嘛?

我们再新建一个测试类,同时在 @Component 中添加新的方法void inject(OtherTest otherTest);

public class OtherTest {
    @Inject
    Brandy mBrandy;

    @Inject
    @Named("CabernetSauvignon")
    Brandy mCSBrandy;

    public OtherTest() {
        DaggerBrandyComponent.create().inject(this);
    }

    @Override
    public String toString() {
        return mBrandy.toString()+"\n"+mCSBrandy.toString();
    }
}

在我们的测试用例中输出System.out.println(new OtherTest().toString());,结果如下所示:

Scope的障眼法

可以看到我们又生成了一个新的蒸馏器,也就是说我们的 Distiller 类并不是一个真正的单例,但是在一个用例(一个被注入类)的范围内,他确实是一个“单例”。这也就是我所说的,@Scope 注释的障眼法(@Singleton 的实质就是一个 @Scope)。

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}

@Scope 字面意思是范围,实际使用的效果我们可以看出在相同范围内,只会存在一个该实例。那么这个范围到底是什么?我的理解是:调用注入者的生命周期,就是这个标注的范围。比如 TestDagger 类调用了 DaggerBrandyComponent.create().inject(this); 进行了注入,在这个类的生命周期里,会复用 Distiller 类的实例。

可以很容易的证明我上面的这段文字,我们自行实现一个 Scope 注解:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface Lalala {}

运行的结果与上面是完全相同的!充分证明了 @Scope 注解的本质就是在同生命周期中复用有注解的实例。
:提供实例的方法、或者含有@Inject的类,其 Scope 名称必须与对应的 Component 完全一致!

只要一个蒸馏器

上面说了,所谓的 Singleton 注解只是一个官方已经定义好的 @Scope,那我们怎么才能真正的实现一个单例的蒸馏器呢?

首先我们在介绍一个 @Component 注解的参数 dependencies (依赖),通过依赖我们可以将注射器进行“继承”,Show me the code!

新建一个接口,这个接口中有一个方法,返回值是 Distiller,注意其中的 Scope 注解,使用的是与 Distiller 类相同的注解。那么这个 Component 可以为我们提供 Distiller 类的注入。注意其中方法名是可以随便写的,这跟 @Provides 注解是一样的,Dagger 只关心返回值。

@Component
@Singleton
public interface BaseComponent {
    Distiller anyName();
}

修改 BrandyComponent 类如下:

@Component(modules = BrandyModule.class,dependencies = BaseComponent.class)
@Lalala
public interface BrandyComponent {
    void inject(MainActivity mainActivity);
    void inject(TestDagger testDagger);
    void inject(OtherTest otherTest);
}

注意这里我使用的是刚刚自行创建的 Scope 注解,因为 Component 的 Scope 不能相同。重新编译代码后会发现报错了,是因为我们原来使用的注入是DaggerBrandyComponent.create().inject(this);,当我们为 BrandyComponent 添加依赖后,就不能再使用 create 方法来生成 Component 的实例了,只能使用 builder 方法来构建,而且我们还必须要为 builder 添加 baseComponent(BaseComponent baseComponent) 这一方法;

前面我们已经说到,@Scope 注解的本质是在同生命周期内复用实例。我们在一个单例中实现 BaseComponent (单例模式的生命周期就是软件的生命周期),那么这个注射器可以注入的实例就将都是单例模式。

为了验证我们的说法:我们创建一个单例:

public class Singleton {

    private BaseComponent mBaseComponent;

    private Singleton() {
        mBaseComponent = DaggerBaseComponent.create();
    }

    private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return SingletonInstance.INSTANCE;
    }

    public BaseComponent getBaseComponent() {
        return mBaseComponent;
    }
}

将原来的注入方法DaggerBrandyComponent.create().inject(this);修改为

DaggerBrandyComponent
        .builder()
        .baseComponent(Singleton.getInstance().getBaseComponent())
        .build()
        .inject(this);

再次运行代码:


实现了真正的单例

这次我们就真正实现了被注入对象单例了!

在 Android 中我们有一个现成的单例模式可用,那就是我们的 Application 类,我们只要写下如下代码就可以实现上述效果:

public class MyApp extends Application {

    private BaseComponent mBaseComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        mBaseComponent = DaggerBaseComponent.create();
    }

    public BaseComponent getBaseComponent() {
        return mBaseComponent;
    }
}

如果 BaseComponent 需要使用 Module 的话,就将 BaseComponent 实例获取方式修改为: mBaseComponent = DaggerBaseComponent.builder().baseModule(new BaseModule()).build();

总结要点:

  1. Component 与 Module 的 Scope 必须相同;
  2. Component 与 被依赖的 Component 的 Scope 必须不同;
  3. 如果 Component 有依赖,则只能使用 builder 方式来构建 Component 对象,同时必须传入被依赖的 Component;
  4. 被依赖的 Component 提供的能被注入的实例,需要在接口中用方法声明。

好啦,本篇文章到此也就告一段落了,对于 Dagger 的使用,相比你也已经有了一定的了解了,本文示例代码在DaggerDemo,大家可以参考着代码阅读本文,会对理解有更好的帮助!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容