前言
本篇文章是读者『sososeen09』的投稿,Android群英传刚刚开放投稿,有什么好的建议和意见,还请大家多多和我交流,继续欢迎大家多多投稿~
本篇文章讲解的是Dagger2,相信很多开发者对它都并不陌生,我们公司的内部分享,也有同事介绍过Dagger2。这篇文章并不是讲解Dagger2的基础使用,而是通过讲解它的使用以及套路,帮助大家更好的理解如何高效的使用Dagger2,相信大家看完,一定会对Dagger2的理解更加深刻!
谷歌开发维护的Dagger2出来有很长时间了,目前在很多开源项目上也能看到它的身影。看了一些文章和项目,发现Dagger2的入门虽然有些难,但还是有一些规律可循的。
对于开源的第三方项目,我认为都是有套路可循的,找到这个套路,入门就不会难了,难的是如何更好的在实际开发项目中灵活运用。而灵活运用必然是建立在对这些开源框架深刻理解的基础之上。
关于Dagger2这种依赖注入框架的好处在这只简单的提一下
那么Dagger2相对于其他的依赖注入框架,有哪些有点和缺点呢?
优点:
缺点:
下面会展开对Dagger2的介绍,看看Dagger2都有哪些套路。文中的代码都是从自己写的一个Demo中提取,文末会给出项目地址。
想要理解Dagger2,首先要理解Dagger2中的注解,至少先了解一下,否则理解Dagger2会有障碍。Dagger2的注解比较多,但主要的会有下面7种。
最简单的Dagger2运用只采用两个注解@Inject和@Component即可。因为本身@Inject就自带两个作用。 如一个User类:
public class User {
public String name;
//用这个@Inject表示来表示我可以提供User类型的依赖
@Inject
public User() {
name = "sososeen09";
}
public String getName() {
return name;
}
}
在需要依赖的的目标类中标记成员变量,在这里我们这个目标类是OnlyInjectTestActivity。
@Inject //在目标类中@Inject标记表示我需要这个类型的依赖
User mUser;
在Component中,Component内有一个方法是inject(OnlyInjectTestActivity onlyInjectTestActivity),参数OnlyInjectTestActivity表示目标类,也就是把依赖实例注入该类中,必须精确,不能用父类代替。查看了一下编译后生成的代码,最后给变量赋值按照“类名.变量”来的。比如我们需要给mUser赋值,那么调用inject方法后,是按照“OnlyInjectTestActivity.mUser=xxx”来的。至于inject这个方法名是可以改的,但是谷歌推荐用inject。
代码就写好了,此时Make Project就会在build文件夹内生成对应的代码。我们的OnlyInjectComponent接口会生成一个以Dagger为前缀的DaggerOnlyInjectComponent类。
采用这个DaggerOnlyInjectComponent就能完成依赖对象的注入。可以在Activity的onCreate方法中调用如下代码,初始化注入。这样的话OnlyInjectTestActivity 中的成员变量mUser就完成了注入过程(也就是变量赋值过程)。
DaggerOnlyInjectComponent.builder().build().inject(this);
整个依赖注入过程就结束了,是不是很简单。 @Inject提供依赖虽然很简单,但是它也有缺陷:
举个例子,还是User类,有一个带参的构造方法,
@Inject
public User(String name) {
this.name = name;
}
如果用@Inject标记带参的构造方法,如String类型。那么这个String类参数也需要依赖,也就是说需要其它地方告诉Dagger可以提供一个String类型的对象。这个时候@Inject就无能为力了,你没办法修改String类给它的构造方法加上@Inject标记啊。所以必须要用我们另一个强大的标记@Module了。
采用@Module标记的类提供依赖是一个常规套路,我们在项目中运用最多的也是这种方式。前面已经提到,@Module标记的类主要起到一个管理作用,真正提供依赖实例靠的是@Provides标记的带返回类型的方法。
这次以一个Person类为例,代码跟User类似,构造方法没有用@Inject标记。目标类中需要给一个Person类型的成员变量mPserson赋值。代码就不贴出来了,具体的可以查看原文链接或项目。
我们用Module提供Person实例,Module代码如下:
@Module
public class DataModule {
@Provides
Person providePerson() {
return new Person();
}
}
上面的代码也算是一个固定套路了,用@Module标记类,用@Provides标记方法。如果想用Module提供实例,还要有一个Component,如我们下面的PersonComponent 。这个PersonComponent 与纯粹用@Inject方式提供依赖不同,还需要有一个modules属性指向DataModule 。这是告诉Component我们用DataModule 提供你想要的类型的实例。其它的方式相同。
@Component(modules = DataModule.class)
public interface PersonComponent {
void inject(ModuleTestActivity moduleTestActivity);
}
编译之后,我们就可以在目标类ModuleTestActivity 中进行初始化注入了。
与纯粹用@Inject提供实例不同。新增加了一个dataModule方法,参数是DataModule类型的。因为PersonComponent需要依赖DataModule提供实例,当然也需要一个DataModule对象了。在这里,需要说明一点:如果DataModule只有一个默认的无参构造方法,我们是可以不用调用dataModule方法的,而且此时我们还可以用一个更简单的方式来替代,采用create()方法。之前讲的纯粹用@Inject提供依赖实例的方式也可以这样。
DaggerPersonComponent.create().inject(this);
这样的话,依赖注入过程结束。mPerson已经被赋值。
完成上面两步之后我们会不会有这样的思考:如果同时有@Module和@Inject构造方法来提供同一类型的实例,Dagger会调用哪个呢?这就牵涉到@Module和@Inject的优先级问题了。
总结一句话就是:在提供依赖对象这一层面上,@Module级别高于@Inject。
虽然写的简单,但也是经过验证的,请大家放心这个结论。
讲完了@Mudule和@Inject的优先级问题,我们可以总结一下Dagger是如何查找所需的依赖实例进行注入了。
步骤如下:
如果你要问:我既没有@Module提供的实例,也没有@Inject标记的构造方法会怎样?很简单,编译期就会报错。 Dagger2的报错提醒还是很好的,能帮你快速的查找出问题所在。
@Qualifier这个限定符在项目中也会比较有用,比如之前讲的在Android中同样的Context,有ApplicationContext还有Activity的Context,就可以用自定义的“@ForApplication”和“@ForActivity”限定符来表示。Dagger2中已经有一个定义好的限定符@Named,长的是这个样子:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
下面还是以Person为例,并且我们自定义一个限定符来看看这个东西具体如何使用,Person有两个构造方法。
public Person(String sex) {
this.sex = sex;
}
public Person() {
sex = "太监";
}
可以看到,默认的Person对象是一个“太监”,那么我想要一个“妹子”,还想自定义一个,如何区分呢?
我们先仿造@Named自定义一个限定符@PersonQualifier:
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface PersonQualifier {
}
在DataModule,我们额外提供“妹子”和”qualifier sex”,代码如下:
Component我们就不贴出来了,跟上面一样。在需要依赖的类中:
@Inject
Person mPerson;
@Inject
@Named("female")
Person mPersonFemale;
@Inject
@PersonQualifier
Person mPersonQualifier;
调用DaggerPersonComponent初始化注入之后,我们就可以看到,所有的成员变量都正确赋值了。
个人觉得,@Scope的作用主要是在组织Component和Module的时候起到一个提醒和管理的作用。 Dagger2中有一个默认的作用域@Singleton,是这么写的:
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
乍一看到Singleton,都会觉得Dagger2这么吊,标记一下就能创建单例了?后来研究了一下发现,这个@Singleton并没有创建单例的能力,或者也可以说不是我们常规用的那种单例,直接用AClass.getInstance()就能获取一个AClass的一个全局单例。
@Singleton的使用也很简单,在Module内的provide方法加上这个注解即可,比如我们想提供一个SingletonTestEntity实体对象。在DataModule中,就可以这么写:
@Module
public class DataModule {
@Provides
@Singleton
SingletonTestEntity provideSingletonTestEntity() {
return new SingletonTestEntity("测试单例");
}
}
有一个SingletonTestComponent ,我们之前说过@Component可以标注接口,也可以标注抽象类,我们就把这个SingletonTestComponent 改成了抽象类。
需要说明的是:DataModule中的SingletonTestEntity 使用@Singleton标注了,那么对应的Component也必须采用@Singleton标注,表明它们的作用域一致,否则编译的时候会报作用域不同的错误。
我们新建一个SingletonTestActivity,显示mSingletonTestEntity这个对象,有一个Button用于启动一个新的SingletonTestActivity,这样我们就可以看每次这个mSingletonTestEntity是不是同一个,是的话当然就能说明我们创建的这个实体对象是单例了。
我一开始是这么初始化的:
DaggerSingletonTestComponent.builder().build().inject(this);
然后我发现每次启动新的Activity,拿到的SingletonTestEntity不是同一个,让我很困惑,还以为是用的姿势不对。后来研究了一下生成的代码,也查了一些文章,发现真的是我用的姿势不对。初始化依赖注入应该这么写,SingletonTestComponent必须是一个单例的:
SingletonTestComponent.getInstance().inject(this);
这样的话,用这个单例的Component注入器去注入的依赖才能算是单例的。
说到这大家可能也看到了,这怎么能是单例呢?我们常规理解的单例是类在虚拟机中只有一个对象。而我们这个依赖实例其实只是每次都由同一个Component注入器对象提供。如果重新生成一个Component对象,注入的依赖实例就不再是同一个。
我们还可以仿造@Singleton自定义一个作用域,如@PerActivity,用来表示跟Activity的生命周期一致。具体的用法就不再介绍了,跟@Singleton用法一样,项目中可以看。
小结一下:
通过上述的讲解可以发现,Dagger2也没有想象的那么难啊。但是不得不说,Dagger2入门并不难,想要灵活运用就不容易了。主要的原因就是在实际开发中我们要好好的组织Component,那么多页面,那么多类,我们怎么写Component就有学问了。Component有3种组织方式:
下面这张图,是Android-CleanArchitecture项目Component组织方式:
可以看到这么划分的思想是:
说到这,我想提一下上面为了演示@Singleton的用法,我们并没有在Application中进行初始化。个人觉得,实际开发中用@Singleton标记来表示在App生命周期内全局的对象,然后用自定义的@PerActivity、@PerFragment等来表示跟Activity、Fragment生命周期一致比较好。
现在我们采用依赖、包含、继承的方式来演示Component的组织方式。就提供一个全局的ApplicationContext好了,只是演示,没必要那么复杂。
我们有个AppModule,用来提供一个App生命周期内全局的Context,AppModule的初始化依赖于Appliation。
//构造依赖Appliation
public AppModule(Application application) {
this.application = application;
}
@Provides
@Singleton
Context getAppContext() {
return application;
}
AppComponent是这样的,
@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {
//其他的依赖想要用这个Context,必须显式的暴露。
Context context();
}
想要其它依赖这个AppComponent的Component并使用使用全局的Appliation Context,我们必须显式地暴露出去。
这个AppComponent接口内没有inject方法,因为具体地注入哪个类,是由依赖它的Component决定的。
我们自定义Appliation,在onCreate中初始化一个单例AppComponent,并提供方法返回这个AppComponent单例对象。
再次强调:这个AppConponent只能初始化一次
现在我们有一个ActivityComponent,需要依赖这个AppComponent ,那么写出来是这个样子:
Component依赖另一个Component,它们的作用域不能相同。所以我们自定义了一个@PerActivity作用域。
我们的这个ActivityComponent本身也可以需要Module提供依赖实例,如ActModule,这个ActModule没有作用域。内部有个@Provides标记的方法用来返回ActEntity类型的对象。至于代码我们就不贴出来了,很简单。
初始化注入的时候我们会发现多了一个appComponent方法用来传入AppComponent类型的参数,这个与传递Module参数的方法形式类似。
上面我们也提到了,包含的方式不需要父Component显示的暴露对象就可以拿到它能提供的所有依赖。在这点上要比依赖方式好一点。在ActivityComponent中我们可以包含其它的Component。只需有一个返回被包含Component类型的方法就好。
//包含SubComponent
ActSubComponent getActSubComponent();
上面的ActSubComponent 是被包含,它需要有个@Subcomponent注解。如果是包含的方式,作用域可以与包含它的Component一致。这点跟依赖方式不一样。
@Subcomponent
@PerActivity
public interface ActSubComponent {
void inject(SubFragment subFragment);
}
一般会在Activity中初始化这个ActivityComponent对象。然后我们在Activity内部的Fragment可以拿到对应的这个SubComponent。
2.7.3 继承
如果AComponent继承了BComponent,那么AComponent就必须提供BComponent中需要的Module。
下面的例子是ExtendTestComponent继承了ActivityComponent,需要提供ActModule和AppModule。 有的人可能会问ActivityComponent并没有AppModule啊,那是因为ActivityComponent依赖了AppComponent,由AppComponent提供了AppModule。
例子中的ExtendTestComponent有@Singleton标记,这是因为AppModule中有@Singleton作用域。如果ActModule中有一个@PerActivity作用域的话,这个Component还必须要再加上@PerActivity。
初始化注入是这个样子,多了两个方法appModule()和actModule()。
哦了,先到这吧。
通过上面的内容,至少可以了解Dagger2中常用的一些注解以及组织方式,在这里做一下简单的总结:
本文内容都是个人理解与实践,难免有错误和遗漏之处,欢迎指正,共同学习。