看了一些文章和项目,发现Dagger2的入门虽然有些难,但还是有一些规律可循的。
对于开源的第三方项目,我认为都是有套路可循的,找到这个套路,入门就不会难了,难的是如何更好的
在实际开发项目中灵活运用。而灵活运用必然是建立在对这些开源框架深刻理解的基础之上。
关于Dagger2这种依赖注入框架的好处在这只简单的提一下
依赖的注入和配置独立于组件之外
依赖对象是在一个独立、不耦合的地方初始化。当初始化方式改变的时候修改的代码少。
依赖注入使得单元测试更加简单。
那么Dagger2相对于其他的依赖注入框架,有哪些有点和缺点呢?
优点:
编译期生成代码,生成的代码像手写的一样。而且如果有错误会在编译期报出。
错误可追踪
易于调试。
缺点:
缺少灵活性,很多代码要按照既定的规则写
没有动态机制。
下面会展开对Dagger2的介绍,看看Dagger2都有哪些套路。文中的代码都是从自己写的一个Demo中提取,文末会
给出项目地址。
1 Dagger2的注解
想要理解Dagger2,首先要理解Dagger2中的注解,至少先了解一下,否则理解Dagger2会有障碍。Dagger2的注解
比较多,但主要的会有下面7种。
@Inject:@Inject注解有两个作用,1是在需要依赖的类(下面这样的类都会称为目标类)中标记成员变量
告诉Dagger这个类型的变量需要一个实例对象。2是标记类中的构造方法,告诉Dagger我可以提供这种类型
的依赖实例。
@Provide: 对方法进行注解,都是有返回类型的。用来告诉Dagger我们想如何创建并提供该类型的依赖实例
(一般会在方法中new出实例)。用@Provide标记的方法,谷歌推荐采用provide为前缀。
@Module: @Module这个注解用来标记类(一般类名以Module结尾)。Module主要的作用是用来集中管理
@Provide标记的方法。我们定义一个被@Module注解的类,Dagger就会知道在哪里找到依赖来满足创建类
的实例。modules的一个重要特征是被设计成区块并可以组合在一起。(例如可以在App中看到多个组
合在一起的modules)
@Component:Components是组件,也可以称为注入器。是@Inject和@Module之间的桥梁,主要职责是把
基本原理.png
二者组合在一起。@Component注解用来标记接口或者抽象类。所有的components都可以通过它的modules知道
它所提供的依赖范围。一个Component可以依赖一个或多个Component,并拿到被依赖Component暴露出来的
实例,Component的dependencies属性就是确定依赖关系的实现。
@Scope: 作用域。Scopes非常有用,Dagger2通过自定义注解来限定作用域。这是一个非常强大的功能,
所有的对象都不再需要知道怎么管理它自己的实例。Dagger2中有一个默认的作用域注解@Singleton,通常
在Android中用来标记在App整个生命周期内存活的实例。也可以自定义一个@PerActivity注解,用来表明生命
周期与Activity一致。换句话说,我们可以自定义作用域的粒度(比如@PerFragment, @PerUser等等)。
@Qualifier: 限定符,也是很有用。当一个类的类型不足以标示一个依赖的时候,我们就可以用这个注解。
例如,在Android中,我们需要不同类型的Context,我们可以自定义标识符注解“@ForApplication”和
“@ForActivity”。这样的话,当注解一个Context的时候,我们可以用这个标识符来告诉Dagger我们想提供
哪一种Context。Dagger2里面已经存在一个限定符@Named注解。
@SubComponent:如果我们需要父组件全部的提供对象,这时我们可以用包含方式而不是用依赖方式,相比于
依赖方式,包含方式不需要父组件显式显露对象,就可以拿到父组件全部对象。且SubComponent只需要在父
Component接口中声明就可以了。
2 Dagger2的套路 2.1 最简单的运用
最简单的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。
/** * 没有modules和dependencies的情况下,纯粹用@Inject来提供依赖 */ @Component() public interface OnlyInjectComponent { /** * 必须有个目标让Component知道需要往哪个类中注入 * 这个方法名可以是其它的,但是推荐用inject * 目标类OnlyInjectTestActivity必须精确,不能用它的父类 * 这是Dagger2的机制决定的 */ void inject(OnlyInjectTestActivity onlyInjectTestActivity); }
代码就写好了,此时Make Project就会在build文件夹内生成对应的代码。我们的OnlyInjectComponent接口会生成
一个以Dagger为前缀的DaggerOnlyInjectComponent类。
采用这个DaggerOnlyInjectComponent就能完成依赖对象的注入。可以在Activity的onCreate方法中调用如下代码
,初始化注入。这样的话OnlyInjectTestActivity 中的成员变量mUser就完成了注入过程(也就是变量赋值过程)。
DaggerOnlyInjectComponent.builder().build().inject(this);
整个依赖注入过程就结束了,是不是很简单。
@Inject提供依赖虽然很简单,但是它也有缺陷:
只能标记一个构造方法,如果我们标记两个构造方法,编译的时候就会报错。因为不知道到底要用哪一个构造
提供实例。
不能标记其它我们自己不能修改的类,如第三方库,因为我们没办法用@Inject标记它们的构造函数。
举个例子,还是User类,有一个带参的构造方法,
public class User { public String name; /** * 用@Inject标记的构造函数如果有参数,那么这个参数也需要其它地方提供依赖。 * 但是@Inject有一个缺陷,就是对于第三方的类无能为力。因为我们不能修改第三方的构造函数, * 所以对于String还有其他的一些我们不能修改的类,只能用@Module中的@Provides来提供实例了 */ @Inject public User(String name) { this.name = name; } public String getName() { return name; } }
代码中的注释写的很清楚了,如果用@Inject标记带参的构造方法,如String类型。那么这个String类参数也需要
依赖,也就是说需要其它地方告诉Dagger可以提供一个String类型的对象。这个时候@Inject就无能为力了,你没
办法修改String类给它的构造方法加上@Inject标记啊。所以必须要用我们另一个强大的标记@Module了。
2.2 采用@Module提供依赖
采用@Module标记的类提供依赖是一个常规套路,我们在项目中运用最多的也是这种方式。前面已经提到,@Module标
记的类主要起到一个管理作用,真正提供依赖实例靠的是@Provides标记的带返回类型的方法。
这次以一个Person类为例,Person类如下,构造方法没有用@Inject标记:
public class Person { private String sex; public Person(String sex) { this.sex = sex; } public Person() { sex = "太监"; } public String getSex() { return sex; } }
我们用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 中需要一个Person类型的依赖:
@Inject Person mPerson;
编译之后,我们就可以在目标类ModuleTestActivity 中进行初始化注入了。
DaggerPersonComponent.builder().dataModule(new DataModule()).build().inject(this);
与纯粹用@Inject提供实例不同。新增加了一个dataModule方法,参数是DataModule类型的。因为PersonComponent
需要依赖DataModule提供实例,当然也需要一个DataModule对象了。在这里,需要说明一点:如果DataModule只有
一个默认的无参构造方法,我们是可以不用调用dataModule方法的,而且此时我们还可以用一个更简单的方式来
替代,采用create()方法。之前讲的纯粹用@Inject提供依赖实例的方式也可以这样。
//如果DataModule有一个无参构造方法 DaggerPersonComponent.create().inject(this);
这样的话,依赖注入过程结束。mPerson已经被赋值。
完成上面两步之后我们会不会有这样的思考:如果同时有@Module和@Inject构造方法来提供同一类型的实例,
Dagger会调用哪个呢?这就牵涉到@Module和@Inject的优先级问题了。
2.3 @Module和@Inject的优先级问题
虽然优先级的问题,我们可以直接说出来,但还是亲手做一个实验好了,这样印象必定会更深刻。而且当你告诉别人
这个结论的时候,你就可以挺直腰板的说就是这样,不然心里总虚啊。
新建一个实体类PriorityTestEntity,用@Inject标记构造方法,代码如下:
public class PriorityTestEntity { private String name; @Inject public PriorityTestEntity() { name = "我是@Inject注解提供的对象"; } public PriorityTestEntity(String name) { this.name = name; } public String getName() { return name; } }
在DataModule中有一个方法,返回值类型是PriorityTestEntity:
@Module public class DataModule { @Provides PriorityTestEntity providePriorityTestEntity() { return new PriorityTestEntity("我是module提供的对象"); }
新建Component,PriorityTestComponent,有一个inject()方法,注入目标类
PriorityTestActivity。
@Component(modules = {DataModule.class}) public interface PriorityTestComponent { //注入目标类PriorityTestActivity void inject(PriorityTestActivity priorityTestActivity); }
剩下的就是在PriorityTestActivity 中进行初始化注入了,步骤都是跟之前讲的一样,真真的是套路啊,没什么难度。
在PriorityTestActivity 中 //@Inject标记成员变量 @Inject PriorityTestEntity mPriorityTestEntity; //初始化注入 DaggerPriorityTestComponent.create().inject(this); //调用代码验证 mTvShowUser.setText(mPriorityTestEntity.getName()); //最后TextView上会显示"我是module提供的对象"
总结一句话就是:在提供依赖对象这一层面上,@Module级别高于@Inject。
2.4 初始化依赖实例的步骤
讲完了@Mudule和@Inject的优先级问题,我们可以总结一下Dagger是如何查找所需的依赖实例进行注入了。
步骤如下:
查找Module中是否存在创建该类型的方法(前提是@Conponent标记的接口中包含了@Module标记的Module类,
如果没有则直接找@Inject对应的构造方法)
若存在方法,查看该方法是否有参数
若不存在参数,直接初始化该类的实例,一次依赖注入到此结束。
若存在参数,则从步骤1开始初始化每个参数
若不存在创建类方法,则查找该类型的类中有@Inject标记的构造方法,查看构造方法中是否有参数
若构造方法中无参数,则直接初始化该类实例,一次依赖注入到此结束。
若构造方法中有参数,从步骤1依次开始初始化每个参数。
如果你要问:我既没有@Module提供的实例,也没有@Inject标记的构造方法会怎样?很简单,编译期就会报错。
Dagger2的报错提醒还是很好的,能帮你快速的查找出问题所在。
2.5 @Qualifier限定符有什么神奇的作用
@Qualifier这个限定符在项目中也会比较有用,比如之前讲的在Android中同样的Context,有ApplicationContext
还有Activity的Context,就可以用自定义的“@ForApplication”和“@ForActivity”限定符来表示。
Dagger2中已经有一个定义好的限定符@Named,长的是这个样子:
@Qualifier @Documented @Retention(RUNTIME) public @interface Named { /** The name. */ String value() default ""; }
下面还是以Person为例,并且我们自定义一个限定符来看看这个东西具体如何使用。
public class Person { private String sex; public Person(String sex) { this.sex = sex; } public Person() { sex = "太监"; } public String getSex() { return sex; } }
可以看到,默认的Person对象是一个太监,那么我想要一个“妹子”和“汉子”,还想自定义一个,如何区分呢?
我们先自定义一个限定符@PersonQualifier:
@Qualifier @Retention(RetentionPolicy.RUNTIME) public @interface PersonQualifier { }
在DataModule,我们额外提供“汉子”、“妹子”和"qualifier sex",代码如下:
@Module public class DataModule { //一个默认的 @Provides Person providePerson() { return new Person(); } //采用@Qualifier注解,表示我可以提供这种标识符的Person @Provides @Named("male") Person providePersonMale() { return new Person("汉子"); } @Provides @Named("female") Person providePersonFemale() { return new Person("妹子"); } @Provides @PersonQualifier Person providePersonByQualifier() { return new Person("qualifier sex"); } }
Component长这个样子:
@Component(modules = DataModule.class) public interface PersonComponent { void inject(ModuleTestActivity moduleTestActivity); }
在需要依赖的类中
//在ModuleTestActivity中成员变量这样标记。 @Inject Person mPerson; //这么多对象,如果需要特定的对象,用@Qualifier标识符注解,@Named是自定义的一个标识符注解 @Inject @Named("male") Person mPersonMale; @Inject @Named("female") Person mPersonFemale; @Inject @PersonQualifier Person mPersonQualifier;
然后注入,
DaggerPersonComponent.builder().dataModule(new DataModule()).build().inject(this);
查看Person对象的性别:
... mTvShowUser.setText(mPerson.getSex()); ... mTvShowUser.setText(mPersonMale.getSex()); ... mTvShowUser.setText(mPersonFemale.getSex()); ... mTvShowUser.setText(mPersonQualifier.getSex()); ...
就可以看到,我们拿到了我们想要的对象。
2.6 @Scope作用域怎么用
个人觉得,@Scope的作用主要是在组织Component和Module的时候起到一个提醒和管理的作用。
Dagger2中有一个默认的作用域@Singleton,是这么写的:
@Scope @Documented @Retention(RUNTIME) public @interface Singleton {}
乍一看到Singleton,都会觉得Dagger2这么吊,标记一下就能创建单例了?后来研究了一下发现,这个@Singleton
并没有创建单例的能力,或者也可以说不是我们常规用的那种单例,直接用AClass.getInstance()就能获取一个
AClass的一个全局单例了。
下面我们看看,这个@Singleton怎么用,又是如何获取单例的。
我们有一个实体SingletonTestEntity,
public class SingletonTestEntity { private String desc; @Inject public SingletonTestEntity(String desc) { this.desc = desc; } public String getDesc() { return desc; } }
在DataModule中,
@Module public class DataModule { @Provides @Singleton SingletonTestEntity provideSingletonTestEntity() { return new SingletonTestEntity("测试单例"); } }
有一个SingletonTestComponent ,我们之前说过@Component可以标注接口,也可以标注抽象类,我们就把这个
SingletonTestComponent 改成了抽象类。
需要说明的是:DataModule中的SingletonTestEntity 使用@Singleton标注了,那么对应的Component也必须
采用@Singleton标注,表明它们的作用域一致,否则编译的时候会报作用域不同的错误。
@Component(modules = {DataModule.class}) @Singleton//这个Component的@Scope要和对应的Module的@Scope一致 public abstract class SingletonTestComponent { /** * /@Component不仅可以注解接口也可以注解抽象类,为了方便测试单例,把Component改为抽象类, * 实际开发中可以在Application中创建单例。 */ public abstract void inject(SingletonTestActivity singletonTestActivity); /** * SingletonTestComponent必须是单例的, * 否则怎么能保证不同的Component对象提供同一个依赖实例呢? */ private static SingletonTestComponent sComponent; public static SingletonTestComponent getInstance() { if (sComponent == null) { sComponent = DaggerSingletonTestComponent.builder().build(); } return sComponent; } }
我们新建一个SingletonTestActivity,显示mSingletonTestEntity这个对象,有一个Button用于启动一个新的
SingletonTestActivity,这样我们就可以看每次这个mSingletonTestEntity是不是同一个,是的话当然就能说明
我们创建的这个实体对象是单例了。
//成员变量 @Inject SingletonTestEntity mSingletonTestEntity; //展示mSingletonTestEntity这个对象 mTvShowUser.setText(mSingletonTestEntity.getDesc() + ": " + mSingletonTestEntity);
上面少了一步,就是初始化注入,我一开始是这么初始化的:
DaggerSingletonTestComponent.builder().build().inject(this);
然后我发现每次启动新的Activity,拿到的SingletonTestEntity不是同一个,让我很困惑,还以为是用的姿势不对。
后来研究了一下生成的代码,也查了一些文章,发现真的是我用的姿势不对。初始化依赖注入应该这么写:
SingletonTestComponent.getInstance().inject(this);
这样的话,我们的这个注入器SingletonTestComponent就首先实实在在地变成一个单例了,用这个Component去注入的
依赖才是单例的。
说到这大家可能也看到了,这怎么能是单例呢?我们常规理解的单例是类在虚拟机中只有一个对象。而我们这个依赖
实例其实只是每次都由同一个Component注入器对象提供,重新生成一个Component对象的话注入的依赖实例就不再
是同一个。
我们还可以仿造@Singleton自定义一个作用域,如@PerActivity,用来表示跟Activity的生命周期一致:
@Scope @Retention(RetentionPolicy.RUNTIME) public @interface PerActivity { }
具体的用法就不再介绍了,跟@Singleton用法一样,项目中可以看。
总结一下:想要用Component只提供同一个实例对象,就必须保证Component只初始化一次。
2.7 重点和难点——组织Component
通过上述的讲解可以发现,Dagger2也没有想象的那么难啊。但是不得不说,Dagger2入门并不难,想要灵活运用就
不容易了。主要的原因就是在实际开发中我们要好好的组织Component,那么多页面,那么多类,我们怎么写
Component就有学问了。
Component有3种组织方式:
依赖方式——一个Component可以依赖一个或多个Component,采用的是@Component的dependencies属性。
包含方式——这里就用到了我们@SubComponent注解,用@SubComponent标记接口或者抽象类,表示它可以被包含。
一个Component可以包含一个或多个Component,而且被包含的Component还可以继续包含其他的Component。
说起来跟Activity包含Fragment方式很像。
继承方式——用一个Component继承另外一个Component。
下面这张图,是Android-CleanArchitecture项目Component组织方式:
组织方式.png
可以看到这么划分的思想是:
我们需要一个ApplicationComponent,管理在App的全局实例,保证在App生命周期内,对象只有一个。例如网络
请求的全局HttpClient。
ActivityComponent: 负责管理生命周期跟Activity一样的组件。
UserComponent: 继承于ActivityComponent的组件,通常会在Activity内部的Fragment中使用。
说到这,我想提一下上面为了演示@Singleton的用法,我们并没有在Application中进行初始化。个人觉得,实际开发
中用@Singleton标记来表示在App生命周期内全局的对象,然后用自定义的@PerActivity、@PerFragment等来表示
跟Activity、Fragment生命周期一致比较好。
现在我们采用依赖、包含、继承的方式来演示Component的组织方式。就提供一个全局的ApplicationContext好了,
只是演示,没必要那么复杂。
Module类是这样的,
@Module public class AppModule { private final Application application; public AppModule(Application application) { this.application = application; } @Provides @Singleton Context getAppContext() { return application; } }
AppComponent是这样的,
@Singleton @Component(modules = {AppModule.class}) public interface AppComponent { /** * Exposed to sub-graphs. * 其他的依赖想要用这个Context,必须显式的暴露。 * 因为,其它依赖这个的Component需要Context,然后这个Context会去AppModule中找对应的Context * 与方法名无关,只与返回类型有关 * 举个例子:小弟B依赖大哥A,A有一把杀猪刀。哪天小弟碰上事了,找大哥借一把刀, * 如果大哥把刀藏起来不给小弟用,小弟会因为找不到刀用很崩溃的。(程序编译报错), * 所以必须是大哥把刀拿出来给小弟用,小弟才能拿出去用啊。(代码正常) * */ Context context(); }
想要其它依赖这个AppComponent的Component并使用使用全局的Appliation Context,我们必须显式地暴露出去。
这个AppComponent接口内没有inject方法,因为具体地注入哪个类,是由依赖它的Component决定的。
我们自定义Appliation ,
public class App extends Application { private static AppComponent sAppComponent = null; @Override public void onCreate() { super.onCreate(); if (sAppComponent == null) { sAppComponent = DaggerAppComponent.builder() .appModule(new AppModule(this)) .build(); } } public AppComponent getAppComponent() { //向外界的依赖提供这个AppComponent return sAppComponent; } }
再次强调:这个AppConponent只能初始化一次
2.7.1 依赖
现在我们有一个ActivityComponent,需要依赖这个AppComponent ,那么写出来是这个样子:
@PerActivity //@Singleton //不能与依赖的AppComponent的作用域相同,否则会报错 @Component(dependencies = AppComponent.class, modules = ActModule.class) public interface ActivityComponent { void inject(DependenceTestActivity DependenceTestActivity); void inject(SubComponentTestActivity subComponentTestActivity); //包含SubComponent,这样的话该SubComponent也可以拿到ActivityComponent中能提供的依赖。 ActSubComponent getActSubComponent(); }
Component依赖另一个Component,它们的作用域不能相同。所以我们自定义了一个@PerActivity作用域。
我们的这个ActivityComponent本身也可以需要Module提供依赖实例,如ActModule,这个ActModule没有作用域。
至于ActEntity的代码,我们就不贴出来了。
@Module public class ActModule { @Provides ActEntity getActEntity() { return new ActEntity("我是ActEntity"); } }
初始化注入是这个样子:
DaggerActivityComponent.builder() .appComponent(((App) getApplication()).getAppComponent()) .build() .inject(this);
2.7.2 包含
上面的ActSubComponent 是被包含,它需要有个@Subcomponent注解,如果是包含的方式,作用域可以与包含它的
Component一致。
@Subcomponent @PerActivity //如果是包含的方式,作用域与上一层的Component相同也没关系。采用依赖的方式就不行。 public interface ActSubComponent { void inject(SubFragment subFragment); }
初始化注入是这个样子:
在Activity中
mActivityComponent = DaggerActivityComponent .builder() .appComponent(((App) getApplication()).getAppComponent()) .actModule(new ActModule()) .build();
然后在Fragment中拿到这个mActivityComponent :
((SubComponentTestActivity) getActivity()).getActivityComponent() .getActSubComponent() .inject(this);
2.7.3 继承
我们的ExtendTestComponent继承了ActivityComponent,那么ActivityComponent中需要的Module我们就必须
提供。有的人可能会问ActivityComponent并没有AppModule啊,那是因为ActivityComponent依赖了
AppComponent,由AppComponent提供了AppModule。
ExtendTestComponent有@Singleton标记,这是因为AppModule中有@Singleton作用域。如果ActModule中有
一个@PerActivity作用域的话,这个Component必须要再加上@PerActivity。
/** * ExtendTestComponent继承了ActivityComponent, * 如果ActivityComponent中的modules定义了创建实例的方法, * ExtendTestComponent中也必须提供相应的modules。 */ @Singleton @Component(modules = {ActModule.class, AppModule.class}) public interface ExtendTestComponent extends ActivityComponent { void inject(ExtendTestActivity extendTestActivity); }
初始化注入是这个样子:
DaggerExtendTestComponent.builder() .appModule(new AppModule(getApplication())) .actModule(new ActModule()) .build() .inject(this);
哦了,先到这吧。
3 总结
通过上面的内容,至少可以了解Dagger2中常用的一些注解以及组织方式,在这里做一下简单的总结:
@Module提供依赖的优先级高于@Inject
@Singleton并不是真的能创建单例,但我们依然可以保证在App的生命周期内一个类只存在一个对象。
@Singleton更重要的作用是通过标记提醒我们自己来达到更好的管理实例的目的。
Component的作用域必须与对应的Module作用域一致,如果@Module没有标记作用域,就不影响。
Component和依赖的Component作用域范围不能一样,否则会报错。一般来讲,我们应该对每个Component
都定义不同的作用域。
由于@Inject,@Module和@Provides注解是分别验证的,所有绑定关系的有效性是在@Component层级验证。
(在这里提一下,本文没有讲这个具体过程)
本文内容都是个人理解与实践,难免有错误和遗漏之处,欢迎指正,共同学习。
共同学习,写下你的评论
评论加载中...
作者其他优质文章