Dagger2是一款最初由Square公司研发,后交由Google进行维护管理的依赖注入(Dependency Injection DI)框架。
我想之所以其越来越受欢迎,一是其自身的优异。二是当我们了解了对它的使用之后,就会发现它和Android现在盛行的MVP架构可以说是天生一对。
于是当我们看到越来越多的地方开始提及Dagger2这个东西,难免自己就会想要去尝试一下。那么,当我们看着一大堆的关于Dagger2的学习资料时:
难免就会被一些类似“Dagger2从入门到放弃”的关键词吸引,很显然这从一定程度上说明了:想要熟练的掌握Dagger2并不轻松;并且在学习过程中很可能会遇到无数的困惑。然而,对于“入门到放弃”这样的的观点,小弟只想说四个字:。。。。。。深!有!体!会。
所以,本文将试图站在一个“纯菜鸟”的视角上,介绍一些关于Dagger2的使用以及学习过程中遇到的疑惑。让我们开始吧!
我们前文已经说到Dagger2是一个依赖注入框架,所以在学习它的使用之前,我们显然有必要弄清楚依赖注入的概念。
看下面这样一个老套的例子:
上述代码试图还原的场景是:A类某个方法需要借助B类某个方法才能完成。于是很显然在A类当中需要持有B类的对象,这个时候就产生了所谓的依赖。
在这里B类对象的持有方式,是通过直接在A类中使用new来完成的。这肯定是最差的方式。因为,B类的构造函数一旦改动,A类将直接受到牵连。
为了避免这种情况发生,就衍生了所谓的依赖注入。逐步递进,现在,我们首先接着看一种比较“原始“的注入方式:
现在调整了代码,改为使用向构造函数传参来持有B类对象,这样A类和B类就完成了解耦。这时B类的构造器无论如何变动,至少A类代码不会再受影响。
但这种解耦其实也只是相对于“某种程度”上来说的。因为这个时候的耦合只是由A类转移到了第三方,即A类的调用者,比如说C类。看代码:
这个时候我们难免就会考虑,有没有一种方式可以更大程度完成解耦?“没有买卖就没有杀害”,那么现在有了买卖,于是也就出现了依赖注入框架。
现在,我们通过一个简单的例子看一下怎么使用Dagger2对之前的代码进行解耦。在这个过程中,我们将学会两个常用的注解@Inject与@Component。
我们首先来看一下如何通过Dagger2来改造A类:
从上述的代码中可以看到,我们是分别对变量b以及A的构造函数加上了注解@Inject。这个注解可以说是Dagger2中最容易理解的一个:
对变量b加上注解意味着告诉Dagger2,A类依赖了B,但b对象需要你帮我注入到A类当中来,我就不管了。而对构造器声明该注解的意义暂且不提。
接着,是看一下B类是如何来改造的。同理,对B的构造函数加上@Inject的意义我们也暂且不提:
最后,是改造过后的C类的面目:
我们可以发现,这里就是对其需要的A类对象变量添加了注解@Inject。现在我们来回忆一下,就知道添加在A类与B类构造器上的@Inject注解的意义了。
之前我们说到在依赖的变量上添加@Inject的意义在于,告诉Dagger2这个变量的对象需要你负责帮我注入。那么,Dagger2怎么确定如何帮你注入呢?
我们可能会想,Dagger2你自己去找构造器啊?没错,如果你需要注入的类永远只有一个构造器,那么这样做看上去就是行得通的。但这显然不可能。
所以,我们还需要为所依赖的变量对象的类的构造器也添加上@Inject,这就等于告诉Dagger2,这里依赖的对象我希望你通过这种构造器来生成注入。
OK,那么通过Dagger2来设置依赖注入的工作是不是就完成了呢?实际上不难想到是还没有的。这样考虑:
为对象变量添加@Inject注解代表着这是依赖的需求方;而添加在依赖对象所属类的构造器上的@Inject代表这是依赖的提供方。从而不难看到:
现在,我们显然还需要一个“桥梁“把依赖方和提供方给联系起来。在Dagger2当中,这个桥梁就是@Component。所以我们还需要这样的东西:
也就是说,我们需要定义一个接口,然后为其添加上@Component注解,然后声明一个叫做inject()的方法,顾名思义,这个方法的意义很好理解。
可以这样认为:我们说过了component是桥梁,通过其调用inject()的意义就好比:现在要把提供(对象)注入给依赖(变量a)了,而注入的目的地,就是C。
OK,现在准备工作就完成了。重新编译一下项目,Dagger2会自动为我们生成一个刚才定义的Component的实际实现,其默认以Dagger开头。
于是,我们在C的构造器中添加以下一行代码,目的就是在构造C类的对象的时候就进行一系列相关的依赖注入工作:
现在,我们就完成了整个依赖注入工作了。现在我们可以写一个测试类来测试调用c的doSomething方法,看看会发生什么。
以上就是测试的日志输出结果,从中我们很容易简单的分析出整个依赖注入的过程:
- 首先,我们通过C类对象调用其实例方法doSomething,于是C类的构造器率先执行了。
- 而调用C的doSomething方法,由于其依赖A类,所以A类的构造器又执行了。
- 同理,A的doSomething方法又依赖于B类,所以B类的构造器又执行了。
- 这个时候,所有的对象就都注入完成了,于是正式调用B的doSomething方法,从而打印出对应内容。
好的,有可能这个时候,就会第一次冒出“从入门到放弃”的念头了。因为我们发现之前我们使用的很熟练的代码,加入Dagger2后,开始有点懵逼了!
其实这很正常,因为通常人都害怕“改变”。之前的代码我们写过无数,非常熟悉非常亲切。猛的一下换种方式难免会抗拒,何况这种方式看上去并不那么容易理解。所以这个时候,我们急需看到Dagger2带给我们的好处,抵消一下“哥想放弃”的怨念。想象一种情况:
现在因为需求的改变,B类需要依赖另一个新的类D了。那么如果以我们最初的方式,就会出现类似改动:
public B(D d){
this.d = d;
Log.d("Dagger2","Class B construct..");
}
显然,这样做的话,就又会影响其他需要依赖B对象的地方。而因为我们使用了Dagger2,则可以使用如下改动:
由此我们可以发现,本次改动除了B类自身添加依赖,其它的A类,C类都不会受到任何的影响。
好吧,我们发现Dagger2让我们进一步进行了解耦。尝到了一点甜头,我们暂时决定放下“放弃”的念头。一起继续学习!
不知道大家注意到没有,到目前为止,我们涉及到的依赖都有一个共同点,那就是其构造方式在我们控制范围内。现在试想一种新的情况:
如果我们依赖的某个类,来自于外部或者我们控制不了其构造,那么应该怎么办呢?于是现在就该@Module与@Provides上场了。
我们举个最简单的例子,在Android里面有一个我们熟悉的朋友:Context。那么,现在我们修改一下A类,变为了如下所示:
好的,现在我们看到的情况是A类的doSomeThing方法需要依赖于Context对象,所以我们可以通过如上图所示的方法来注入context。
但是,如果我们这里要统一为用Dagger2注入呢?应该如何去做?首先当然还是为依赖的Context对象变量添加@Inject注解:
接下来的工作就与之前有所不同了。因为,很显然我们不会去给Context类的构造器加上@Inject,现在意味着我们需要一种新的方式来提供Context的依赖。所以,我们需要新建一个下面这样的类:
如图所示,这里我们新建了一个叫做ClassAModule的类,加上了@Module注解。这意味着此类是一个专门用来为A类提供依赖的Module类。
接着,我们看到一个被@Provides注解的方法provideContext。顾名思义,它就是用来提供A类所依赖的Context对象的。那么,现在就又回到了熟悉的步骤,编写“桥梁”- Component:
从上图的代码中可以看到,与之前的不同之处在于,这次在@Component注解里声明了,本次向A类注入所需要的依赖的工作,不再是通过构造器,而是通过ClassAModule这个Module类来提供相关依赖对象。同样的,再次编译项目后就得到了对应的Component实际实现。当然调用也发生了轻微改变:
我们总结一下可以发现,通过@Module+@Provides与在构造器上设置@Inject的本质是相同的,都是用来提供依赖;差别只是提供方式不同而已。
分析一下这个过程也就是说:如果我们的某个类中依赖于某些类,同时这些类的构造方式又不是我们能控制的。那么,就可以新建一个@Module类用以提供这些依赖。而@Module类提供这些依赖的方式则是声明对应返回类型的provideXXX方法。但是:这个时候我就更想要”从入门到放弃”了!
因为回忆一下,之前说到@Inject的使用时,我们能很明显的体会到Dagger2带来的好处。但这里显然就不是了,我们对比一下两种注入方式:
定义处:
调用处:
我们可以看到这两处怎么看都是使用Dagger2过后写的代码更多一点。并且!使用Dagger2过后,我们还需要维护额外的Module与Component类。
试想现在A类又添加了新的依赖,那么两种方式同样都会涉及到A类和MainActivity的代码改动;而Dagger2还会涉及Module与Component的改动。
一脸懵逼的我想来想去,觉得似乎除了是要统一依赖注入的方式(Dagger2)之外;唯一能想到的好处似乎就是把类的依赖全部抽离到了单独的Module类中。当然,这只是个人看法,说不得正确,还希望有精通依赖注入和Dagger2的大腿能够指点迷津。
现在,我们接着考虑一种新的情况。假设现在有类A,B,C,类A和类B当中都依赖于C,但C有两种构造器,A和B分别是依赖于不同的构造器。那么,有了之前的使用基础,我们最初可能会简单的这样考虑:
那么,这里抛开Dagger2如何去区分类A,B到底是需要哪种构造器构造依赖对象不同。编译项目首先会遇到如下的错误:
很显然,编译器已经告诉你为C类定义了多个@Inject修饰的构造器,这是不合法的。这个时候该怎么办?我们想起了还有另一种提供依赖的方式:
但是这里就要注意了,回忆一下:之前说A类依赖于Context时,我们在Module类里通过provideContext方法来提供这个依赖。
但我们需要明白的是,这里的方法命名只是一种规范,而非规则。也就是说可以随意定义这个方法名,那么Dagger是如何确定这就是提供依赖的地方呢?
很显然,既然不是通过方法命名,自然就是通过返回类型了。但这里我们看到两个方法的返回类型都是C。显然我们还是没有完全解决我们的问题。
这个时候,就需要使用到Dagger2的另一个注解@Qulifier了。说白了这个注解就是用来起到一个标识的作用的。它的使用方式如下:
可以看到这个注解本身就是用来修饰注解的,我们这里定义了两个标示注解DefaultConstructor与ConstructorWithString。
之后的步骤我想大家都可以猜到怎么做了,没错,先分别给我们Module里的两个provide方法加上标示:
最后以A类为例,假设A类依赖的是C类无参的默认构造器,那么修改A类的代码:
最终经过调用,运行程序就得到了我们想要的日志打印信息:
最后,我们一起来看Dagger2中最难理解,最让人想要“从入门到放弃”的一个注解:@Scope的使用。我们首先看这样的代码:
然后是Activity的代码:
也就是说,这里我们在MainActivity中引用了两个A的对象a1和a2。在经过注入过后,我们打印这两个对象的信息,输出如图:
从输出结果我们可以看到,这里的分别构建了两个全新的对象。那么,有的时候我们希望控制某个对象的生命周期。比如说:
现在我们希望在当前Activity只存在唯一一个A类对象,并且其生命周期随Activity消亡而消亡。那么就需要借助@Scope来实现了:
有了之前@Qulifier的经验,我相信上面的代码并不难理解。之后的工作也很简单,我们分别在两处需要的地方加上这个注解就搞定了:
随后,我们再次运行程序,得到如下输出结果:
现在我们起码确定了A类的对象在MainActivity中是单例的,那么其生命周期究竟是否与Activity是保持一致的呢?我们进一步验证:
我们新建了一个SecondActivity,然后进行由MainActivity跳转到SecondActivity的操作,得到如下输出:
可以看到,Activity发生跳转后,就注入了新的A类对象。如果你是第一次接触@Scope,我相信你和我一样的疑惑。
因为我们好像就简单的通过@Scope定义了一个注解@ActivityScope,居然就实现了如此强悍的功能?但懵逼的是:
为什么这么轻易就实现了?似乎我们在使用@Scope注解时也没有指定任何有关于生命周期的信息啊?
不用着急,我们接着看。假设现在我们更进一步,想要在整个应用内,A类对象的依赖都是唯一的。也就是所谓的全局单例,那么又该怎么做?
首先,我们再定义一个@Scope注解,用来代表全局生命周期:
接着,新建一个用于注入全局依赖的@Component接口:
然后,当然是将其依赖的Module里的ProvideA()方法也设置Scope:
最后,当然自定义我们自己的Application类,并做相关设置:
可以看到,以上的大部分东西我们都是比较熟悉的,唯一的不同在于:
- Component接口中没有再提供inject方法,相反是有一个返回类型为A的方法。
- 在MyApplication的onCreate方法中调用注入时,因为没有inject()方法,所以是直接通过build方法返回了定义的AppCompent对象。
我相信稍微机智的朋友到了这里就很容猜出出现这种不同的意义了,没错,核心就在于:我们通过MyApplication类里的appComponent对象调用getA方法,就可以返回Dagger2为我们注入到MyApplication的a了。也就是所谓的全局单例。那么,在Activity中我们就通过如下的方式去获取全局对象了:
但是细心一点的朋友可能会说,既然是Dagger2,对象还是通过@inject来注入好点吧!没错,那么现在我们怎么办呢?很简单,修改MainActivityComponent:
没错,这里我们看到原来Component也是可以进行依赖的,方式就是通过设置dependencies。现在Activity中就可以通过正规军的方式依赖A了:
好了,现在回归正题。我将结合自己的理解,试图以一种最简单易懂的方式来讲述@Scope注解为什么能实现这样的作用。我们分析一下:
不知道大家注意到没有,我们在AppComponent与ClassAModule中的provideA()方法上,添加的注解是@ApplicationScope。
然后,在MainActivityComponent上添加的注解时@ActivityScope注解。为什么这样做呢?我们可以测试一下:
其实异常信息描述的很清晰:为AppComponent设定的Scope与其自身依赖的Module中的provideA方法设定的Scope是不一致的。
所以请大家一定记住:@Component与其依赖的@Module的@Scope一定要是保持一致的。Dagger2实际就是通过这个来决定对象的生命周期的。
可以这样理解:当我们为Component设定了scope之后,当我们通过build()方法构建出这个“桥梁”后,它就进入了一段生命周期当中。同时:
与它关联的Module内,只要是同样设定了scope,那么它在这段生命周期范围内就是唯一的,并且生命周期将跟随scope。那么:
因为我们只在自定义的Application中build了一次AppComponent,自然的,设定了scope的依赖对象自然就成为了“全局唯一”的对象。
这样说可能仍然感觉很乱,我们通过一个例子能够更好的理解这个东西,修改MainActivity的代码:
上面的代码有了之前的基础,相信大家都能明白用意。现在再次运行程序,将收到如下的输出信息:
我们看到,这里得到了两个不同的对象。也就是说,现在虽然appComponent依旧是用所谓的@ApplicationScope进行注解的。
但是,上述代码中的对象a2现在的生命周期(域)实际上却是@ActivityScope,也就是与MainActivity相关联的了。
有了之前的基础,这个异常就更容易理解了。简单的说就是:
MainActivityComponent的Scope设定与AppComponent冲突了,而它们本身就不属于同一个等级范围。
换句话说,MainActivityComponent的Scope设定,应该与它本身依赖的Module内的scope保持一致。
相信大家现在就明白了,所谓的@Scope并没有那么神奇。它的作用实际上就是统一管理Component和Module内提供依赖对象的方法。
当设定了Scope之后,Component被build出来后,这里面设定了Scope的依赖对象对于该Component注入的依赖来说就是唯一的。
最后,简单的说一下@Singleton。如果你能理解我之前的解释,那么理解@Singleton就将变得非常容易。举个例子:
我们之前用自己定义的@ApplicationScope来设定AppComponent和provideA方法。那么,我们直接使用@Singleton也是一样的。
因为所谓的@Singleton其实就只是一个@Scope的默认实现而已,一下就是@Singleton的源码:
到了这里,关于dagger2的使用就聊到这里。我只能再次感叹,学习这个东西的确容易让人懵逼和抓狂。希望本文能给您带来一点点的帮助。