自从 Dagger2 出现,个人对 Dagger2 的学习过程也是断断续续的,一直没有系统的总结过,所以也谈不上掌握,还有就是没有投入到项目的使用。
本文的目的:希望能尽量详细的整理总结一下 Dagger2 的相关知识,尽量避免有所遗漏,造成大家需要查找大量的文章进行查漏补缺。以及用由简到繁的几个例子来介绍一下如何使用,以方便日后翻阅。
简介
Dagger2 是什么?为什么要使用?这也是我接触 Dagger2 以来一直有思考的事,在没有一个大概的认识之前,也疑惑过是不是有必要去使用,但是这里我不会说有没有必要,我希望大家能在看完本文之后对 Dagger2 有一个大概的认识。
A fast dependency injector for Android and Java.
Dagger2 是一个 Android 和 Java 的依赖注入框架。而「依赖注入」(Dependency Injection)则是实现「控制反转」(Inversion of Control)的一种方式,是一种面向对象的编程模式,主要目的就是为了降低耦合。
Dagger2 和其他依赖注入框架的区别是:Dagger2 是在编译阶段生成注入代码,不再使用反射,所以不会影响到性能。在应用运行的时候其实运行的是真正的 Java 代码,可以直接断点调试。
使用前需要了解的原则
依赖倒置原则
- 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
依赖注入的方式
- 构造函数注入
- setter注入
- 接口注入
如果对这些原则和「控制反转」有所疑问的话强烈推荐大家看下下面几篇文章,文章不长但是比较益于我们理解。这里也不再举其他的例子了,感觉也举不出更好的例子,但是不建议直接跳过。
使用 Dagger2 前你必须了解的一些设计原则
依赖注入原理
IoC 模式
配置
在 project 的 build.gradle 中添加
dependencies {
//...其他
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'//Android gradle plugin 版本是2.2以上可以省略
}
在 module 的 build.gradle 中添加
apply plugin: 'com.neenbedankt.android-apt'//Android gradle plugin 版本是2.2以上可以省略
dependencies {
compile 'com.google.dagger:dagger:2.10'
apt 'com.google.dagger:dagger-compiler:2.10'//Android gradle plugin 版本是2.2以上,apt 替换成 annotationProcessor
provided 'org.glassfish:javax.annotation:10.0-b28'
}
基本使用
@Inject
该注解是在「需要依赖的类的成员变量」上和「被依赖的成员变量的类的构造方法」上标注的。让 Dagger2 知道这个字段需要依赖注入,这样它就会去构造一个类的实例来满足依赖。注意:标注的成员变量不能为 private;如果有多个构造函数只能标注一个。
public class A {
B mB;
public A(){
mB = new B();
}
}
原来上面的代码就可以改写成,下面这样。
public class A {
@Inject
B mB;
public A(){
//「1」...这里省略了注入的过程,先跳过
}
}
public class B {
@Inject
public B() {
}
}
}
单单上边的改写其实是不够的,因为我们省略了注入的过程,所以要介绍 @Component。
@Component
该注解是在接口或者抽象类上标注的。它的作用就是把「需要被注入的类的实例」注入到「需要依赖注入的目标类」中。
开始编写Component,来实现「1」中省略的注入过程。
@Component
public interface AComponent {
void inject(A a);
}
//----------分割线-------
//修改 A
public class A {
@Inject
B mB;
public A() {
DaggerAComponent.create().inject(this);
}
}
到这里其实我们已经成功的初始化了 A 中的成员变量 mB 了,有的同学可能疑问:我们仅仅是创建了一个带 @Component 注解的 interface,这个 DaggerAComponent 是什么?这里的代码和最初的代码看上去麻烦了不少,不是用 new B() 显得更简单嘛,而且也并没有去除 A 类对 B 类的依赖,说好的「依赖倒置原则」呢?
注意: inject(A a) 方法只能传入「需要注入的目标类」,而不能传入「注入目标类的基类」,因为最终 Dagger2 会在 inject 方法传入的类中查找 @inject 标注的成员变量进行初始化。
这里我先回答第一个问题,DaggerAComponent 是在代码编译时期 apt 给我们生成的代码,所以我们 rebuild 一下项目就会生成以 Dagger 为前缀的 Component;至于第二个问题就先带着疑问往下看吧!
@Module
被该注解标注的类主要用于提供依赖,Module 类其实就是一个简单工厂,里面的方法基本上就是用于创建类的实例的方法,Component 可以在指定的 Module 中查找依赖。
单单用 @Inject 配合 @Component 是不够满足我们的开发需求的,因为:
- 我们没有办法给三方类库添加 @Inject;
- @Inject 所标注的构造方法需要我们自己传入参数的情况;
- 需要被注入的类有多个构造方法的情况。
所以需要用 @Module。
在举例使用 @Module 之前,我们需要了解下 @Provides。
@Provides
该注解在 Module 中创建类的实例的方法上标注。Component 在注入的目标类中找到用 @Inject 标注的成员变量后,Component 就会去 Module 中查找用 @Provides 标注的对应的创建类的实例方法来实现注入。
现在就来举例讲下 @Module 的使用了:
举例1、对应「注入第三方类」的情况
现在有一个第三方类 C ,我们同样需要把 C 注入到 A 中,现在我们继续修改代码。首先我们需要添加一个 AModule 类;
@Module
public class AModule {
@Provides
C provideC(){
return new C();
}
}
然后修改 A 类和之前已经定义好的 AComponent;
@Component(modules={AModule.class})//在指定的 AModule 中查找依赖,可以有多个 Module
public interface AComponent {
void inject(A a);
}
//----------分割线-------
//修改 A
public class A {
@Inject
B mB;
@Inject
C mC;
public A() {
DaggerAComponent.create().inject(this);
//DaggerAComponent.builder().aModule(new AModule()).build().inject(this);
//两种写法都是可以的,只不过 AModule() 本身不需要参数,所以 create() 中直接 builder().build() 的操作
}
}
举例2、对应「被注入的类的构造方法需要传参的」的情况
这里又分两种情况:
- 需要被依赖注入的类的构造函数已经用 @Inject 标注;
添加带参构造的类 D,修改类 AModule 与类 A,AComponent 保持不变;
public class D {
private String name;
@Inject
public D(String name) {
this.name = name;
}
}
@Component(modules={AModule.class})
public interface AComponent {
void inject(A a);
}
@Module
public class AModule {
private String mDName;
public AModule(String dName){
mDName = dName;
}
@Provides
String provideName(){
return mDName;
}
@Provides
C provideC(){
return new C();
}
}
//----------分割线-------
public class A {
@Inject
B mB;
@Inject
C mC;
@Inject
D mD;
public A() {
DaggerAComponent.builder().aModule(new AModule("I am D")).build().inject(this);
}
}
简单分析一下,因为类 A 中的成员变量 mD 被 @Inject 标注,所以初始化 mD 的时候会去找同样被 @Inject 标注的 D 的构造方法,但是它需要一个 String 类型的 name 来初始化,这个 name 从哪里找呢?
其实就是通过我们调用 DaggerAComponent 的 inject(A a) 的时候传入的 new AModule("I am D"),然后通过 provideName() 方法暴露出去,这样 mD 的 name 属性就被赋值成 "I am D" 了。
因为 AModule 的构造也带参数,所以生成的 DaggerAComponent 也不再有 create() 方法,只能通过 builder().aModule(new AModule("I am D")).build() 构造。
- 需要被依赖注入的类的构造函数没有用 @Inject 标注;其实这种情况和「举例1」的情况一致,只是刚好「举例1」中的 C 的构造函数不带参数;这里就简单带过一下。
稍微在上边代码的基础上修改一下。
public class D {
private String name;
public D(String name) {
this.name = name;
}
}
//------------------分割线--------------
@Module
public class AModule {
private String mDName;
public AModule(String dName){
mDName = dName;
}
@Provides
D provideD(){
return new D(mDName);
}
@Provides
C provideC(){
return new C();
}
}
去掉了类 D 构造方法上的注解( 不去掉的话也没事 ),在 provideD() 中 new 出实例 return 出去。
举例3、对应「被注入的类有多个构造方法的情况」
这里我们不再讨论普通的情况,所谓普通的情况就是「举例2.2」的情况,针对不同的构造函数不外乎 new 出不同的实例。现在我们要讨论的是同时存在 @Inject 和在 provideXX() 方法中 new 对象的情况。我们重新修改一下上面的代码。
public class D {
private String name;
public D(String name) {
this.name = name;
}
@Inject
public D(){
this.name = "I am default D";
}
public String getName() {
return name;
}
}
//-------------分割线------------
@Module
public class AModule {
private String mDName;
public AModule(String dName){
mDName = dName;
}
@Provides
D provideD(){
return new D(mDName);
}
@Provides
C provideC(){
return new C();
}
}
//------------分割线--------------
public class A {
@Inject
B mB;
@Inject
C mC;
@Inject
D mD;
public A() {
DaggerAComponent.builder().aModule(new AModule("I am D")).build().inject(this);
Log.d("tag", mD.getName());
}
}
看下上面的代码,最终我们能打印出来的是 "I am default D" 还是 "I am D" 呢?答案是后者,这里就涉及到 Dagger2 依赖注入的步骤了,在说这个步骤之前我先做一些其他方面的补充。
举例4、一些补充
首先、回答一下上面「@Component」一节中遗留下来的一个疑问:我非要满足「依赖倒置原则」,高层次模块不依赖于低层次模块,而依赖于抽象。其实也可以实现,定义一个接口 ID,让类 D 实现接口 ID,然后把类 AModule 中的 provideD() 返回值和类 A 中的成员变量 mD 的类型修改为 ID。
其次、在「举例1」的代码注释中 Component 可以有多个 Module,这里提一下两种方法,注意: 这多个 Module 之间不能有重复方法。
//第一种
@Component(modules={AModule.class, BModule.class})//有多个 Module
public interface AComponent {
void inject(A a);
}
//第二种 ---------也可以通过下面这种方式构建------------
@Module(includes={AModule.class, BModule.class})
public class CModule {
//...
}
@Component(modules={CModule.class})
public interface AComponent {
void inject(A a);
}
Dagger2 依赖注入的步骤
步骤1:查找 Module 中是否存在创建该类的方法。
步骤2:若存在创建类方法,查看该方法是否存在参数步骤2.1:若存在参数,则按从 步骤1 开始依次初始化每个参数
步骤2.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
步骤3:若不存在创建类方法,则查找 @Inject 注解的构造函数,看构造函数是否存在参数
步骤3.1:若存在参数,则从 步骤1 开始依次初始化每个参数
步骤3.2:若不存在参数,则直接初始化该类实例,一次依赖注入到此结束
不难看到,查找类的实例的过程中 Module 的级别要高于 Inject。
到这里相信大家基本可以使用 @Inject、@Component、@Module、@Provides了,也包括他们在 MVP 中的使用,仅仅需要把类 A 替换成 Activity 或者 Fragment,把类 D 替换成 Presenter,把需要通过 AModule 传递给 D(Presenter) 的 String 替换成 IView 之类的 View 层的接口罢了。
完善使用
上面我们仅仅讲了四个注解的用法,当然这还远远不够,如果累了先休息一下,完了我们再继续介绍。
@Qualifier
该注解是为了实现类似 @Named 这种用于区分需要依赖注入相同类型的不同成员变量的功能的注解。简单的来说 @Named 是 @Qualifier 的一种。
举个例子:现在我们需要在类 A 中注入两个 D 类型的成员变量,一个要求是默认的,另一个要求我们自己传入参数,下面看具体代码。
public class D {
private String name;
public D(String name) {
this.name = name;
}
public D(){
this.name = "I am default D";
}
public String getName() {
return name;
}
}
//-------------分割线------------
@Module
public class AModule {
private String mDName;
public AModule(String dName){
mDName = dName;
}
@Named("custom")
@Provides
D provideD(){
return new D(mDName);
}
@Named("default")
@Provides
D provideDefaultD(){
return new D();
}
}
//------------分割线--------------
public class A {
//...省略其他
@Named("default")
@Inject
D mD1;
@Named("custom")
@Inject
D mD2;
public A() {
DaggerAComponent.builder().aModule(new AModule("I am D")).build().inject(this);
Log.d("tag", "mD1:" + mD1.getName() + ”,mD2:" + mD2.getName());
}
}
这里在 A 中的成员变量 mD1 和 mD2 上以及 AModule 中添加的两个返回值为 D 类型的 provide 方法上,分别添加了注解 @Named("default") 和 @Named("custom")。不难猜到打印出来的结果为:
mD1:I am default D,mD2:I am D
我们看下 @Named 的代码:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
所以 @Named 后面跟着的参数为 String 类型,Component 会在指定的 Module 中寻找同时满足类型和 @Named 的依赖进行注入。
当然我们可以自定义一个 @Num 用 int 类型来做参数:至于使用方法就不再多说了。
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Num {
/** The name. */
int value() default 0;
}
@Qualifier 就是针对这种有多个相同类型不同成员变量的注入来进行区分。
注意:@Qualifier 同样可以给 Module 的中的方法的参数标注,以区分一个 Module 中有多个返回值类型相同的方法,又有另一个方法需要其中一个或者多个的返回值作为参数的情况。
@Scope
该注解表示作用域的意思,@Singleton 是它 Dagger2 中已有的一个实现。使用 @Scope就可以更好的管理创建的类实例的生命周期。
既然看到了 @Singleton 那么来实现一个单例吧!
目标:
我要实现在两个 Activity 中实现单例。
使用方法:
- 在 Module 对应的 Provides 方法标注 @Singleton
- 同时在 Component 类标注 @Singleton
public class ActivitySingle {
}
//--------------分割线------------
@Module
public class SingleModule {
@Singleton
@Provides
ActivitySingle provideActivitySingle(){
return new ActivitySingle();
}
//--------------分割线------------
@Singleton
@Component(modules = {SingleModule.class})
public interface SingleComponent {
void inject(SingleOneActivity activity);
void inject(SingleTwoActivity activity);
}
//--------------分割线------------
public class SingleOneActivity extends AppCompatActivity {
private TextView mTv1;
private TextView mTv2;
@Inject
ActivitySingle mActivitySingle1;
@Inject
ActivitySingle mActivitySingle2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_single);
mTv1 = (TextView) findViewById(R.id.tv1);
mTv2 = (TextView) findViewById(R.id.tv2);
DaggerSingleComponent.create().inject(this);
mTv1.setText(mActivitySingle1.toString());
mTv2.setText(mActivitySingle2.toString());
}
}
//SingleTwoActivity 和 SingleOneActivity 除了类名完全一致
运行结果:
注意:这和说好的单例不一样!我们可以看到在同一个 Activity 中我们实例化的 ActivitySingle 对象的确是单例的,但是这不是我们要的结果。可以看到并不是我们标注了 @Singleton Dagger2 就给我们实现了单例了的。
Java 中,单例通常保存在静态域中,这样的单例往往要等到虚拟机关闭时候,该单例所占用的资源才释放。但是,Dagger 通过 @Singleton 创建出来的单例并不保持在静态域上,而是保留在Component 实例中。
原理
在讲「Dagger2 依赖注入的步骤」中我们知道 Component 查找类的实例的过程中 Module 的级别要高于 Inject。
真正创建单例的方法
- 在 AppModule 中定义创建全局类实例的方法;
- 创建 AppComponent 来管理 AppModule;
- 保证 AppComponent 只有一个实例;
简单的来说就是把上例的 SingleComponent 提升到 Application 中管理并保证只有一个。现在有没有体会到 @Scope 的作用域的含义?
具体实现,后续我会拿一个贴近实际使用的例子来讲,这次就先到这里了,因为后续还会牵扯到 Component 之间的拓展和依赖关系以及组织 Component 篇幅会显得比较长,但是我会保证在两篇文章把 Dagger2 整理完。当然这里 @Scope 并没有讲完,我会后续结合例子补充完整。
其实主要还是靠大家自己动手体会,如果有写的不对的地方请提醒我,避免误导了后来的同学,如果觉得我整理的还不错请为我点个赞,谢谢!
更新
参考资料
等不及的同学可以看下下面的文章,尽量按顺序来看。
使用 Dagger2 前你必须了解的一些设计原则
依赖注入原理
IoC 模式
Android:dagger2 让你爱不释手-基础依赖注入框架篇
Android:dagger2 让你爱不释手-重点概念讲解、融合篇
Android:dagger2 让你爱不释手-终结篇
从零开始搭建一个项目(rxJava + Retrofit + Dagger2) --完结篇
Android 常用开源工具(1)-Dagger2 入门
Android 常用开源工具(2)-Dagger2 进阶
详解 Dagger2
Android 单排上王者系列之 Dagger2 注入原理解析