一、前期基础知识储备
1)依赖注入是什么?
依赖注入是面向对象编程的一种设计模式,其目的是为了降低程序耦合,这个耦合就是类之间的依赖引起的。
我们使用各种依赖注入框架比如View注入框架Butternife,Koltin注入框架kodein,更加简易的RoboGuice注入框架等等,都是为了降低类与类之间的耦合。实现依赖注入的方法通常有三种:①构造方法注入;②定义set方法注入;③注解注入。第三方依赖注入框架都是使用注解注入的方式。
2)Dagger2是什么?
Dagger2是Dagger的升级版,是Android目前最好用的依赖注入框架,在编译期间自动生成代码,负责依赖对象的创建。第一代由Square公司共享出来,第二代则是由谷歌接手后推出的。github地址为:google/dagger
二、上代码,具体实现
1)使用Dagger2的优势
代码对比一下,不使用依赖注入和使用依赖注入的代码区别。
三个类:Paper,Paint,TestDraggerActivity,代码如下:
public class Paper {
public String draw(){
return "线稿";
}
}
public class Paint {
private Paper mPaper;
public Paint(Paper paper){
mPaper = paper;
}
public String drawing() {
return mPaper.draw();
}
}
public class DraggerActivity extends AppCompatActivity {
private Paint mPaint;
private Paper mPaper;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mPaper = new Paper();
mPaint = new Paint(mPaper);
Toast.makeText(this, mPaint.drawing(), Toast.LENGTH_SHORT).show();
}
}
一段常规代码,Paint的构造方法中注入Paper实例,日常开发中经常使用构造方法和set方法进行实例注入。然后在Activity中通过new的方式拿到实例对象,再调用对象的方法实现逻辑。但试想,如果Paper,Paint的构造方法改变,比如增减变换形参,这时就会导致所以实例化过Paint和Paper的地方都需要修改,工作量增加了,也违背了开闭原则(在面向对象编程领域中,开闭原则规定“软件中的对象(类,模块,函数等等)应该对于扩展是开放的,但是对于修改是封闭的“)。
下面是使用依赖注入方式的代码,实现同样的逻辑:
public class Paper {
@Inject
public Paper() {
}
public String draw(){
return "线稿";
}
}
public class Paint {
private Paper mPaper;
@Inject
public Paint(Paper paper){
mPaper = paper;
}
public String drawing() {
return mPaper.draw();
}
}
// Dagger2使用时新增一个接口
@Component
public interface TestDraggerActivityComponent {
void inject(TestDraggerActivity activity);
}
public class TestDraggerActivity extends AppCompatActivity {
@Inject
Paint mPaint;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 先编译一下项目 然后Dagger2会自动生成这个类
// Build --> Rebuild Project 或者 Build --> make module
DaggerTestDraggerActivityComponent.create().inject(this);
Toast.makeText(this, mPaint.drawing(), Toast.LENGTH_SHORT).show();
}
}
可以看到Paint和Paper的代码基本不变,只是在类的构造方法上增加了注解@Inject;而Activity中代码减少,并且去掉了此前Paint和Paper的两个实例化的new方法。这样,之后无论Paint和Paper的构造方法如何改变,Activity中的代码都不会受到影响。
注意,在使用Dagger2时需要新建一个接口,命名规则为”Activity名 + Component“,同时在接口的上方加入注解@Component,同时为该接口写入接口方法inject(),方法的参数即为Activity,而这也标注了依赖注入的方向,即Dagger2将@Inject注解构建好的对象注入给谁,这个谁就是inject()方法中的参数。
2)@Inject和@Component两个注解
@Inject
依赖注入中第一个并且是最重要的就是@Inject
注解。用以标记那些需要被依赖注入框架所提供的依赖。在Dagger 2中有3种不同的方式来提供依赖(即添加@Inject注解的位置有三处):
① 构造方法处:
@Inject
public Paper() {
}
告诉Dagger2可以使用这个构造器构建实例对象;
@Inject
public Paint(Paper paper){
mPaper = paper;
}
注入含参数的构造方法所需要的参数,以完成该实例对象的构建;
② 类属性(即对象类型变量)处:
public class TestDraggerActivity extends AppCompatActivity {
@Inject
Paint mPaint;
... ...
}
被标注的属性不能使用private
修饰,否则无法注入。属性注入也是Dagger2中使用最多的一个注入方式。
③ 方法注入:与属性注入类似
public class TestDraggerActivity extends AppCompatActivity {
private Paint mPaint;
... ...
@Inject
public void setPaint(Paint paint) {
mPaint = paint;
}
}
注意要标注在public方法(该方法通常命名为setXXX)上,Dagger2会在构造器执行之后立即调用这个方法。
@Component
@Inject用来标记需要被注入框架注入的方法,属性,构造,而Dagger2则是用@Component来完成依赖注入的,@Component可以说是Dagger2中最重要的一个注解。
@Component
public interface TestDraggerActivityComponent {
void inject(TestDraggerActivity activity);
}
接口方法:void inject(目标类 obj) — Dagger2会从目标类开始查找@Inject注解,自动生成依赖注入的代码,最后类中调用inject(this)即可最终完成依赖的注入。
有些还未使用Dagger2的开发者或许会有疑问:Activity中只有
@Inject
Paint mPaint;
而没有任何关于Paper的声明或注解,那么Paint要如何使用到paper实例呢?其实这个inject过程并不像我们的代码看上去是这种inject套inject的过程,真实过程是这样:在Activity中进行inject()的时候,发现Paint的构造函数被@Inject标注了且带有一个参数paper,然后Dagger2就去寻找Paper发现它的构造函数也被@Inject标注并且无参数,于是Dagger2把Paper的实例注入给Activity,然后再去实例化Paint的时候,用的是已经注入给Activity的那个paper实例。也就是说我们可以这样理解:并不是Paint直接实例化Paper,而是Activity实例化Paper后交给Paint使用的。
总结一下:
① 使用时添加依赖:
api 'com.google.dagger:dagger:2.24',
annotationProcessor 'com.google.dagger:dagger-compiler:2.24'
② 依赖的是什么?注入的是什么?注入给谁?
依赖的是@Inject标注的实例对象,注入的也是@Inject标注的实例对象;
@Inject
Paint mPaint;
注入给@Component注解下接口方法内的目标类:
@Component
public interface TestDraggerActivityComponent {
void inject(TestDraggerActivity activity);
}
③使用注解,Dragger2生成的文件是什么?
我们在使用时,只会使用Dagger2的注解API,但是实际上在编译时Dagger2会生成几份中间文件。如下图:
3)@Module和@Provides两个注解
使用@Inject标记构造器提供依赖是有局限性的,比如说我们需要注入的对象是第三方库提供的,我们无法在第三方库的构造器上加上@Inject注解。
或者,我们使用依赖倒置的时候,因为需要注入的对象是抽象的,@Inject也无法使用,因为抽象的类并不能实例化。我们这里使用如下两个类okhttp3包下的OkHttpClient和RetrofitManager模拟不可改动代码的情况:
public class OkHttpClient implements Cloneable, Call.Factory {
... ...
public OkHttpClient build() {
return new OkHttpClient(this);
}
}
public class RetrofitManager {
private OkHttpClient mClient;
public RetrofitManager(OkHttpClient client) {
mClient = client;
}
public OkHttpClient getmClient(){
return this.mClient;
}
}
这种情况下就需要用到Module了,代码如下:
① 编写Module类并使用@Module标注这个类,编写方法返回值为我们需要inject的类型并使用@Provides标注这个方法;
@Module
public class HttpActivityModule {
@Provides
OkHttpClient provideOkHttpClient() {
return new OkHttpClient();
}
}
②编写Component接口,使用@Component标注这个接口,并使用modules=的方法链接上第一步中编写的Module类;
@Component(modules = HttpActivityModule.class)
public interface HttpActivityComponent {
void inject(HttpActivity activity);
}
之后的步骤就像使用@Inject一样了。
public class HttpActivity extends AppCompatActivity {
@Inject
OkHttpClient mOkHttpClient;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DaggerHttpActivityComponent.create().inject(this);
Toast.makeText(this, mOkHttpClient.getCacheSize(), Toast.LENGTH_SHORT).show();
}
}
复杂的@Module用法,如下:(上面的@Provides标注的构造方法没有参数,如果有参数会有一些不同)
@Module
public class HttpActivityModule {
private int cacheSize;
public HttpActivityModule(int cacheSize) {
this.cacheSize = cacheSize;
}
@Provides
OkHttpClient provideOkHttpClient() {
OkHttpClient client = new OkHttpClient();
client.setCacheSize(this.cacheSize);
return client;
}
@Provides
RetrofitManager provideRetrofitManager(OkHttpClient client) {
return new RetrofitManager(client);
}
}
复杂的情况,Module中其中一个依赖又要依赖另外一个依赖,如果被@Provides标注的方法带有参数,dagger2会自动寻找本Module中其他返回值类型为参数的类型的且被@Provides标注的方法,如果本Module中找不到就会去看这个类的构造参数是否被@Inject标注了(所以一般情况下Module中方法的返回值都不能相同,当然也有办法使多个方法的返回值类型相同,有需要的朋友请自行研究吧)
一般类无法使用@Inject注解时,才会使用@Module,所以使用双@Provides的情况常见。
public class HttpActivity extends AppCompatActivity {
@Inject
RetrofitManager retrofitManager;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
DaggerHttpActivityComponent.builder()
.httpActivityModule(new HttpActivityModule(100))
.build().inject(this);
Toast.makeText(this, retrofitManager.hashCode(), Toast.LENGTH_SHORT).show();
}
}
在Module的构造函数带有参数且参数被使用的情况下,所生产的Component类就没有create()方法了。
三、Dagger2的其他API和搭配MVP使用
下面是Dagger 2的API:
public @interface Component {
Class<?>[] modules() default {};
Class<?>[] dependencies() default {};
}
public @interface Subcomponent {
Class<?>[] modules() default {};
}
public @interface Module {
Class<?>[] includes() default {};
}
public @interface Provides {
}
public @interface MapKey {
boolean unwrapValue() default true;
}
public interface Lazy<T> {
T get();
}
还有在Dagger 2中用到的定义在 JSR-330 (Java中依赖注入的标准)中的其它元素:
public @interface Inject {
}
public @interface Scope {
}
public @interface Qualifier {
}
笔者开发中涉及的上面四个基础注解比较多,想深入研究的话,参考文章:《使用Dagger 2依赖注入 - API》
另外,如果使用Dagger2搭配MVP使用,比如将presenter注入到view层;值得一提的是谷歌不推荐直接将presenter的构造参数添加注解,更加推荐的是将presenter放到Module里进行管理,因为这样代码更加容易管理。
参考文章:《Dagger2从入门到放弃再到恍然大悟》《Dagger2新手入门与使用基础教程》
最后总结一下:MVP、组件化、RxJava中会搭配使用到Dragger2,那么到底有哪些好处?
- 依赖的注入和配置独立于组件之外。
- 因为对象是在一个独立、不耦合的地方初始化,所以当注入抽象方法的时候,我们只需要修改对象的实现方法,而不用大改代码库。
- 依赖可以注入到一个组件中:我们可以注入这些依赖的模拟实现,这样使得测试更加简单。
- Dagger2可以免去在每个使用类的地方都使用构造函数构造一个实例,使用@Inject注解可实现类实例的依赖注入,依赖的构建支持@Module、@Provide注解,用于构建自定义的类和第三方类,并使用@Scope注解来声明依赖的作用域,可以跟acitvity的声明周期绑定在一起,在需要的地方自动实现单例模式。
新文推荐《Dagger.Android — Dagger2进阶》,以上用法是Dagger2的基础用法,在实际项目中,我们会更多地使用Dagger专为Android提供的注入行为,推荐文章在本篇文章的基础上继续深入,希望能有所收获。