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 | //制作白兰地的流程 |
看了这段代码我估计你要骂街了,这是什么鬼!?不是说要介绍依赖注入的优势么,这一大堆的 new 是什么东西?
别急别急,没看我的注释是“制作白兰地的流程”么,我这是在制作一款用解百纳酿造的白兰地啊~,你看我们想要喝白兰地需要先有酒液原浆吧,需要有蒸馏器吧,把原浆蒸馏了才能得到白兰地嘛。而酒液原浆的获得又需要用到葡萄和发酵桶嘛。
其实上面这段代码我们就是模拟了一个相对复杂的实例化过程,可以看到其中的依赖关系如下
1 | |--- 蒸馏器 |
那么在每次我们视图实例化“白兰地”对象时,都需要首先将其他的四个对象先行实例化完毕,实例化一次还好,如果这个实例在上百个页面中都需要使用到呢?但如果我们要是使用了依赖注入框架,将会使这一步骤变得十分容易。
1 |
|
是的,我们只需要在申明对象时,加上一行注解即可。然后在需要使用的地方直接使用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 | buildscript { |
然后在 app 模块下的build.gradle文件中添加apply plugin: 'com.neenbedankt.android-apt'
1 | dependencies { |
然后选择 Sync Now即可;
了解几个小概念
- @Inject
@Inject 有两个功能:
1、注解类的构造方法,其作用可以理解为通过注解将这个类的实例化方法告诉 Dagger;2、在需要使用该实例的地方注解,其作用是告诉调用者,这个被注解的实例由 Dagger 来负责实例化;1
2
3
4
5
6
7
8
9public class Distiller {
public Distiller() {
}
public String toString() {
return "蒸馏器";
}
}1
2
Distiller mDistiller; - @Module
@Module 可以理解为一个生产实例的工厂,他掌握各个需要注入的类的实例化方法,当 Dagger 需要为某个类注入实例时,会到 @Module 注解的类中,查找这个类的实例化方法。当然这一过程是需要通过使用 @Provides 注解的有返回值的方法,来告知 Dagger 的。1
2
3
4
5
6
7
public class BrandyModule {
public Grape provideGrape() {
return new Grape("解百纳");
}
} - @Component
@Component 用来注解一个接口,在编译的时候会生成 Dagger+文件名 的新Java文件。Component可以理解为注射器,它是连接被注入的类与需要被注入的类之间的桥梁。1
2
3
4//这里的modules参数可以是多个,用于告诉“注射器”都有哪些实例可以被注射到目标类中
public interface BrandyComponent {
void inject(MainActivity mainActivity);
} - @Provides
在提供实例的方法上注解,用于告诉 Dagger 这是一个用于注入的实例。方法名可以随便,Dagger 是通过方法的返回值来将其添加到依赖列表的。1
2
3
4
public Grape provideGrape() {
return new Grape("解百纳");
}
看个小例子
如果你按照上面的步骤写下了代码,运行代码你会发现报了一个空指针错误,这是因为我们还没有为我们要注入的对象与被注入的类建立联系。我们需要在使用被注入实例之前调用如下方法**DaggerBrandyComponent.create().inject(this);
,注意这里的参数 this ,这个参数的值由我们刚刚定义的接口 BrandyComponent 中声明的方法的参数决定。为了方便测试,我新建了一个 TestDagger 类,并在接口中添加了一个方法void inject(TestDagger testDagger);
**
1 | public class TestDagger { |
这样做是为了可以方便的在测试用例中进行测试,而不用将程序运行在手机或者模拟器上。
可以看到,下面输出了“蒸馏器”,这说明我们对这一实例的注入成功了。
不得不说的Module
看了上面的例子,你是不是觉得有点意思了?但是要注意的是,上面的那种方法适用于无参的构造方法(当然也可以有参数,但是对应的参数的构造方法上也要有 @Inject 注解)。Talk is cheap,show me the code!
为了验证刚刚提到的那一点,我们来为蒸馏器的构造方法添加一个新的参数 Heater 加热器!
1 | public class Distiller { |
注意上面的代码,Heater 类的构造方法没有使用 @Inject 注解,我们运行一下看看效果。
程序抛出错误,错误的内容是:在没有使用 @Inject 注解构造方法或者 @Provides 注解一个方法时无法提供 Heater 的实例。我们只需要在 Heater 类的构造方法上也加上 @Inject 注解就可以了。
通过上面的例子,你应该了解了我们可以为构造方法参数的构造方法添加 @Inject 注解来实现注入。(绕口令:八百标兵奔北坡,北坡炮兵并排跑,炮兵怕把标兵碰,标兵怕碰炮兵炮)。
但是如果我们的参数是第三方的类呢?比如参数是一个 String 呢?我们不可能去 String 类的构造方法中添加注解。这时候就需要用到 Moudle 类了。
Module 的代码上面我们已经写过了,我们来看一下
1 |
|
我们的 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(多次绑定),难道说 Dagger 只能注入一种实例么??那他的局限性也太大了吧?这时候 @Named 注解就需要粉墨登场啦~
@Named 注解用于给 @Provides 注解提供别名,在使用的时候也需要加上 @Named 注解,Dagger 就知道我们需要的是具体哪个实例了。
1 |
|
可以看到我们现在可以通过 @Named 注解来活动不同的葡萄了,那需要使用 Wine 类如果希望使用赤霞珠作为参数应该怎么办呢,如下所示:
1 |
|
我们新增加一个提供赤霞珠原浆的方法,在其参数中使用了@Named("CabernetSauvignon")
来指定,这个参数是赤霞珠。完整的代码如下所示:
1 |
|
现在我们就可以品尝到使用赤霞珠葡萄制作的白兰地啦~
总结要点:
- @Inject 有两种用途;
- 对于不能使用 @Inject 注解的类,将该类的实例化方法使用 @Provides 注解;
- 对于同一个类的不同实例化方法,使用 @Named 注解;
- @Named 注解还可以注解 Provides 方法的参数;
我们不需要那么多的蒸馏器
我们将 Distiller 的 toString() 方法进行修改:
1 | public class Distiller { |
再次运行程序:
发现问题了吗?我们的白兰地居然使用了不同的蒸馏器,这很不合理,我们的酿酒作坊只需要一个蒸馏器就可以了,完全不需要对每瓶酒都使用一个新的蒸馏器。也就是说我们的 Distiller 类应该是一个单例!
如果你看过其他文章你应该会知道有一个注解 @Singleton,这个注解的字面就是单例,那么我们使用该注解来注释我们的 Distiller 类以及我们的 BrandyComponent 接口。再次运行程序,结果如下:
1 |
|
你可能会惊叹:我的天呐!真是魔法!!!我们居然通过一个注解就实现了单例模式!?还学什么7种单例模式的实现方式啊,以后都用这个注解不就都搞定了嘛?
我们再新建一个测试类,同时在 @Component 中添加新的方法void inject(OtherTest otherTest);
1 | public class OtherTest { |
在我们的测试用例中输出System.out.println(new OtherTest().toString());
,结果如下所示:
可以看到我们又生成了一个新的蒸馏器,也就是说我们的 Distiller 类并不是一个真正的单例,但是在一个用例(一个被注入类)的范围内,他确实是一个“单例”。这也就是我所说的,@Scope 注释的障眼法(@Singleton 的实质就是一个 @Scope)。
1 |
|
@Scope 字面意思是范围,实际使用的效果我们可以看出在相同范围内,只会存在一个该实例。那么这个范围到底是什么?我的理解是:调用注入者的生命周期,就是这个标注的范围。比如 TestDagger 类调用了 DaggerBrandyComponent.create().inject(this);
进行了注入,在这个类的生命周期里,会复用 Distiller 类的实例。
可以很容易的证明我上面的这段文字,我们自行实现一个 Scope 注解:
1 |
|
运行的结果与上面是完全相同的!充分证明了 @Scope 注解的本质就是在同生命周期中复用有注解的实例。
注:提供实例的方法、或者含有@Inject的类,其 Scope 名称必须与对应的 Component 完全一致!
只要一个蒸馏器
上面说了,所谓的 Singleton 注解只是一个官方已经定义好的 @Scope,那我们怎么才能真正的实现一个单例的蒸馏器呢?
首先我们在介绍一个 @Component 注解的参数 dependencies (依赖),通过依赖我们可以将注射器进行“继承”,Show me the code!
新建一个接口,这个接口中有一个方法,返回值是 Distiller,注意其中的 Scope 注解,使用的是与 Distiller 类相同的注解。那么这个 Component 可以为我们提供 Distiller 类的注入。注意其中方法名是可以随便写的,这跟 @Provides 注解是一样的,Dagger 只关心返回值。
1 |
|
修改 BrandyComponent 类如下:
1 |
|
注意这里我使用的是刚刚自行创建的 Scope 注解,因为 Component 的 Scope 不能相同。重新编译代码后会发现报错了,是因为我们原来使用的注入是DaggerBrandyComponent.create().inject(this);
,当我们为 BrandyComponent 添加依赖后,就不能再使用 create 方法来生成 Component 的实例了,只能使用 builder 方法来构建,而且我们还必须要为 builder 添加 baseComponent(BaseComponent baseComponent) 这一方法;
前面我们已经说到,@Scope 注解的本质是在同生命周期内复用实例。我们在一个单例中实现 BaseComponent (单例模式的生命周期就是软件的生命周期),那么这个注射器可以注入的实例就将都是单例模式。
为了验证我们的说法:我们创建一个单例:
1 | public class Singleton { |
将原来的注入方法DaggerBrandyComponent.create().inject(this);
修改为
1 | DaggerBrandyComponent |
再次运行代码:
这次我们就真正实现了被注入对象单例了!
在 Android 中我们有一个现成的单例模式可用,那就是我们的 Application 类,我们只要写下如下代码就可以实现上述效果:
1 | public class MyApp extends Application { |
如果 BaseComponent 需要使用 Module 的话,就将 BaseComponent 实例获取方式修改为: mBaseComponent = DaggerBaseComponent.builder().baseModule(new BaseModule()).build();
总结要点:
- Component 与 Module 的 Scope 必须相同;
- Component 与 被依赖的 Component 的 Scope 必须不同;
- 如果 Component 有依赖,则只能使用 builder 方式来构建 Component 对象,同时必须传入被依赖的 Component;
- 被依赖的 Component 提供的能被注入的实例,需要在接口中用方法声明。
好啦,本篇文章到此也就告一段落了,对于 Dagger 的使用,相比你也已经有了一定的了解了,本文示例代码在DaggerDemo,大家可以参考着代码阅读本文,会对理解有更好的帮助!