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框架也是这样的);

酿酒大师教你酿酒

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

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

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

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

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

1
2
3
4
5
    |--- 蒸馏器
白兰地
| |--- 葡萄
|--- 原浆
|--- 发酵桶

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

1
2
3
4
5
6
7
8
9
10
11
12
@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'

1
2
3
4
5
6
7
8
9
10
11
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'

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

看个小例子

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

1
2
3
4
5
6
7
8
9
10
11
12
13
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 加热器!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 的代码上面我们已经写过了,我们来看一下

1
2
3
4
5
6
7
8
@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 就知道我们需要的是具体哪个实例了。

1
2
3
4
5
@Provides
@Named("CabernetSauvignon")
public Grape provideOtherGrape() {
return new Grape("赤霞珠");
}

默认的注入实例

使用别名的注入实例

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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() 方法进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 接口。再次运行程序,结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@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);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)。

1
2
3
4
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}

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

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

1
2
3
@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 只关心返回值。

1
2
3
4
5
@Component
@Singleton
public interface BaseComponent {
Distiller anyName();
}

修改 BrandyComponent 类如下:

1
2
3
4
5
6
7
@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 (单例模式的生命周期就是软件的生命周期),那么这个注射器可以注入的实例就将都是单例模式。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);修改为

1
2
3
4
5
DaggerBrandyComponent
.builder()
.baseComponent(Singleton.getInstance().getBaseComponent())
.build()
.inject(this);

再次运行代码:
实现了真正的单例
这次我们就真正实现了被注入对象单例了!

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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,大家可以参考着代码阅读本文,会对理解有更好的帮助!