什么是Dagger2
安卓应用在初始化对象的时候经常需要处理各种依赖关系。比如说网络访问中使用Retrofit,Gson,本地存储中使用shared preference。无一例外,我们都都需要在使用它们的地方进行实例对象构建,而且其中还可能存在着各种各样的继承依赖关系。
依赖注入(Dependency Injection,简称DI)是用于削减计算机程序的耦合问题的一个法则。对象在被创建的时候,由一个调控系统内所有对象的外界实体将其所依赖的对象的引用传递给它。也可以说,依赖被注入到对象中。
Dagger2 正是一个依赖注入框架,使用代码自动生成创建依赖关系需要的代码。减少很多公式化代码,更容易测试,降低耦合,创建可复用可互换的模块。
关于依赖注入,看个简单的比较图,左边是没有依赖注入的实现方式,右边是手动的依赖注入:
关于依赖注入的优势可以参考这篇文章从零开始的Android新项目4 - Dagger2篇
关于Dagger1和Dagger2区别可以看下详解Dagger2
Dagger2的优点
- 全局对象实例的简单访问方式
和ButterKnife 库定义了view,事件处理以及资源的引用一样,Dagger2 提供全局对象引用的简易访问方式。声明了单例的实例都可以使用@inject
进行访问。比如下面的MyTwitterApiClient 和SharedPreferences 的实例:
|
|
- 复杂的依赖关系只需要简单的配置
Dagger2 会通过依赖关系并且生成易懂易分析的代码。以前通过手写的大量模板代码中的对象引用将会由它给你创建并传递到相应对象中。因此你可以更多的关注模块中构建的内容而不是模块中的对象实例的创建顺序。
让单元测试和集成测试更加方便
因为依赖关系已经为我们独立出来,所以我们可以轻松的抽取出不同的模块进行测试。依赖的注入和配置独立于组件之外。因为对象是在一个独立、不耦合的地方初始化,所以当注入抽象方法的时候,我们只需要修改对象的实现方法,而不用大改代码库。依赖可以注入到一个组件中:我们可以注入这些依赖的模拟实现,这样使得测试更加简单。作用域实例(Scoped instances)
我们不仅可以轻松的管理全局实例对象,也可以使用Dagger2中的scope定义不同的作用域。(比如根据user session,activity的生命周期)
Dagger2的引用
- 在整个项目的build.gradle中加入:
|
|
- 在app/build.gradle中分别加入:
|
|
|
|
需要注意的是provided代表编译时需要的依赖,Dagger的编译器生成依赖关系的代码,并在编译时添加到IDE 的class path中,只参与编译,并不会打包到最终的apk中。apt是由android-apt插件提供,它并不会添加这些类到class path中,这些类只用于注解解析,应当避免使用这些类。
配置问题
- 如果在项目中同时用了Butterknife,在Build时会报注释冲突。
解决方法:(在modulebuild.gradle文件中添加如下代码)
|
|
- 运行、或编译出错后可能找不到Dagger生成的代码,根据错误提示修改后,可能需要重新ReBuild项目才能正常
- Build后代码在项目–>app–>build–>generated–>source–>apt–>debug目录下
最后点击Build–>Make Project就可以开始使用Dagger2了。
创建单例(singleton)
接下来一步一步的分析Dagger2的使用,先来把Dagger2中的注解讲解一下。如果有点不清晰,请接着往下看,然后再回来看一遍。
注解
@Module
用来修饰类,表示此类里面的方法用来提供依赖,所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例的时候,就知道从哪里去找到需要的依赖。modules的一个重要特征是它们设计为分区并组合在一起(比如说,在我们的app中可以有多个组成在一起的modules)
@Component
这个注解用于构建接口,被Dagger2用于生成用于模块注入的代码。需要定义依赖的模块(或组件) , 这里定义了那些图依赖应当公开可见(可注入),我们的组件可以注入哪里。@Component是连接@Module和@Inject的桥梁。
@Inject
Java定义,在需要依赖的地方使用这个注解。告诉Dagger这个构造方法,成员变量或者函数方法需要依赖注入。这样,Dagger就会构造一个这个类的实例并满足他们的依赖。
@Provide
在Modules中,我们定义的方法是用这个注解,以此来告诉Dagger我们想要构造对象,并通过标记的方法。提供这些依赖。
@Singleton
当前提供的对象将是单例模式 ,一般配合@Provides一起出现
@Scope
Java定义,Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。它使得依赖非常类似与单例。注解的依赖是单例的,但是也与组件的声明周期有关(不是整个应用)。
@Qualifier
Java定义,限定类型,相当于帮助我们创建依赖的”tag”,用于标记依赖的返回类型相同的不同接口。
@MapKey
这个注解用于定义依赖集合(映射和集)。
接着看看Dagger2 的流程:
首先看看下面这段代码,我们需要使用Okhttp,Gson,Retrofit和Gson做一个Twitter 客户端的网络访问。
|
|
可以看到上面使用缓存cache用到了Application的context,这也是在android中使用非常多的上下文对象。我们的第一个Dagger模块AppModule.java(使用@Module进行类注解),将会提供Application 的context引用。我们使用@Provides注解告诉Dagger providesApplication()这个方法是Application的实例的提供者。使用@Singleton注解告诉Dagger整个生命周期中只会被初始化一次。
|
|
接着看看,下面这段代码我们进行了了Gson,Cache,OkHttpClient以及Retrofit 的实例化,这些方法的返回类型都会在定义到依赖关系(依赖表 dependency graph)中。在这里我们需要关注的是三个注解的@Module,@Provides,@Singleton的定义位置。
|
|
Dagger通过@inject注解提供了获取实例引用的方法,通过调用@inject会让Dagger2 在依赖关系(依赖表 dependency graph)中找到对应的引用并赋值给该字段。比如下面的例子就会返回MyTwitterApiClient,SharedPreferences的实例对象。
|
|
可以看到上面通过InjectorClass.inject(this)把当前activity对象注入到InjectorClass,那么InjectorClass是什么呢?
在Dagger2 中 ,注入类(injector class)被称作组件(Component),我们通过inject方法传递activity,service或者fragment对象到注入类component中。比如下面这个类。我们通过@Component注解当前类,并且把之前的两个模块也添加到component中。( Components从根本上来说就是一个注入器。)
|
|
那么你就会好奇这个注解类是怎么完成整个注入的呢?
Dagger2中很重要的一点就是它会为@Component注解的类生成代码。它会在类的前面添加上Dagger前缀(比如DaggerTwitterApiComponent.java),也就是这个类负责初始化依赖关系(依赖表 dependency graph)中的实例,并为注解了@Inject 的字段执行注入操作。
初始化组件(Instantiating the component)
初始化组件操作应当在Application中进行操作,因为这些实例在整个application生命周期中只会被实例化一次。
|
|
可以看到的是我们直接使用NetComponent生成的类DaggerNetComponent并且生成的方法appModule和netModule完成了两个对应module的初始化。因为这里我们继承了Application并作出了修改,所以需要在AndroidManifest.xml中作出修改。
|
|
在activity中,我们需要获取component并且调用inject()方法。注意需要将获取的Application强制转换为MyApp。这也完成了上面InjectorClass.inject(this);代码的替换。到这里就完成了整个Dagger2的依赖注入流程。
|
|
组件(Component)
这个注解用于构建接口,该接口把所有封装在一起。这里,我们定义需要依赖的模块或组件。这里定义了那些图依赖应当公开可见(可注入),我们的组件可以注入哪里。
Dagger会按照上面接口生成一个实现类,生成类以Dagger为前缀,提供builder()来生成实例。(调用方法参考上面的示例)
示例代码中@Component使用了2个模块,可以向GithubClientApplication注入依赖,并使其他3个依赖公共可见:
|
|
而且@Component可以依赖其他组件,可以定义作用域:
|
|
注入(Inject)
首先最重要的DI是@Inject注解。作为JSR-330标准的一部分,标记那些依赖注入框架提供的依赖。Dagger2中,有3种不同的方式提供依赖:
- 构造器注入:
|
|
所有参数都从依赖图中获取。@Inject
注解用在构造器还使得这个类成为依赖图的一部分。这意味它可以也随时注入,例如:
|
|
本例中的限制是,我们只能对类中的一个构造器使用@Inject
注解.
- 域注入
另一个方法是对特殊域使用@Inject
注解:
|
|
本例中注入过程称为“手工”,方式如下:
|
|
该调用之前,我们的依赖值为null。
域注入的限制是域不能声明为private。为什么?简而言之,生成的代码会明确的调用它们,像这样:
|
|
- 方法注入
最后一个使用@Inject提供依赖的方式是注解类中的公共方法:
|
|
所有方法参数由依赖图提供。但是为什么我们需要方法注入呢?当我们需要传类实例自身(this引用)来注入到依赖时,我们需要提供函数注入。函数注入在构造器调用后立即调用,因此这意味着我们需要传递完全构造的this。
如上例中的Watches
也是由Dagger提供依赖,而它是需要执行register来绑定LoginActivityPresenter做初始化的时候。
依赖提供(Provides)
提供依赖的方式除了上面的构造函数上@Inject外,还有@Provides。
在Modules中,我们定义的方法是用这个注解,以此来告诉Dagger我们想要构造对象,并通过标记的方法。提供这些依赖。
为什么要使用@Provides,因为默认情况下,Dagger满足依赖关系是通过调用构造方法得到的实例。
但是有时因为@Inject 的局限性,我们不能使用@Inject。
比如构造方法有多个、三方库中的类我们不知道他是怎么实现的等等。
例如下面代码中的SharedPreferences,我们使用@Provides 返回一个创建好的实例,这样做也显得灵活不是吗?
|
|
注意:
- 按照习惯 @Providers方法都会用provide作为前缀,@Module类都用Module作为后缀。
- 如果@Provides方法有参数,这个参数也要保证能够被Dagger得到(例如通过其他的@Provides方法或者@Inject注解的构造方法。如果是@Provides方法,却又是在Dependencies Component中提供,要保证Dependencies Component的方法公共,即在Dependencies Component中提供了接口)
限定类型(Qualified types)
如果对于不同的对象有同样的返回类型,我们可以使用@Named修饰符注解。你需要在提供单例的地方(@Provides注解)和注入的地方(@Inject注解)都使用@Named注解。
比如,对于同样的返回OkHttpClient ,这里提供不同的方法,和java中多态一样,只不过这里需要额外通过@Named注解来标注:
|
|
|
|
如下,@Named是在Dagger中预先定义好的限定类型修饰符,你也可以创建自己的修饰符注解,加上如下示例注解:
|
|
作用域(Scopes)
Scopes可是非常的有用,Dagger2可以通过自定义注解限定注解作用域。@Singleton是被Dagger预先定义的作用域注解( scope annotation )。没有指定作用域的@Provides方法将会在每次注入的时候都创建新的对象。同样的,你也可以定义自己的Scope注解。
|
|
提醒
- 注解的地方有:Component类、@Provides一起、@Inject注解的构造函数对应的类
- 使用作用域的注解后,对象只会被初始化一次,之后的每次都会被直接注入相同的对象。(关于为什么可以做到单例,有兴趣的可以参考Dagger2使用攻略中关于Scope的分析)
- 所有自定义作用域行为是一样的(从代码角度) - 他们保持单一对象实例。
- 也用在图校验过程,该过程有助于尽快捕获依赖图的结构问题。
组件依赖(Component Dependencies)
上面的例子我们创建了application的全局单例.如果我们想在内存中总是拥有多个组件(例如在activity和fragment生命周期,用户登录注册创建的component),我们可以使用组件依赖(Component Dependencies),使用组件依赖有下面几个考虑点:
- 两个依赖的组件不能共享作用域,比如两个组件不能共享@Singleton作用域。依赖的组件需要定义自己的作用域。
- 尽管Dagger2 有创建作用域实例的能力,你也需要创建和删除引用来满足行为的一致性。Dagger2 不会知道任何底层的实现。
- 当创建依赖组件的时候,父组件需要显示的暴露对象给子组件。比如子组件需要知道Retrofit 对象,也就需要显示的暴露出来。
- 一个没有作用域(unscoped )的组件不可以依赖有作用域的组件
|
|
子组件(Subcomponents)
除了依赖关系,也可以使用子组件进行对象关系(对象表/图 object graph)继承。和组件之间添加依赖关系一样,子组件也有自己的生命周期,也会在所有对其应用不在的时候被垃圾回收,也有同样的作用域限制。区别于组件依赖的不同点主要是:
- 需要在父组件的接口中声明(在接口中定义的方法对于生成的对象是可访问的。)。
- 能够获取父组件的所有元素(不仅仅是在接口中声明的元素)。
比如下面这段代码:
|
|
在上面的例子中,子组件的实例在每次我们调用newMyActivitySubcomponent()的时候都会被创建。
使用子模块去注入一个activity:
|
|
映射和集(MapKey)
这个注解用于定义依赖集合(映射和集)。没用到,直接看示例吧!
自定义一个注解
|
|
提供依赖
|
|
使用
|
|
@MapKey注解现在只支持两种类型的键值 - String和Enum。
Lazy 类
Lazy类是实现懒加载,调用的时候才创建实例,通过Lazy对象实现,得到对象的实例使用get()方法。例如:
|
|
最后来梳理一下Dagger2 中的一些注意点:
- Components从根本上来说就是一个注入器,也可以说是@Inject和@Module的桥梁。 Components可以提供所有定义了的类型的实例,比如:我们必须用@Component注解一个接口然后列出所有的@Modules组成该组件,如 果缺失了任何一块都会在编译的时候报错。@Component接口定义了对象提供者(module)和对象之间的联系,也表述了一种依赖关系。
- 由于Dagger2使用生成的代码去访问字段,所以字段使用了Dagger2 是不允许标注为private的。(即用@Inject注解的需要注入的字段)
- Dagger2 基于JSR 330(为了最大程度的提高代码的复用性、测试性和维护性,java的依赖注入为注入类中的使用定义了一整套注解(和接口)标准。Dagger1和Dagger2(还有Guice)都是基于这套标准,给程序带来了稳定性和标准的依赖注入方法。)
- 使用@inject注解表示依赖关系可以用于三个地方。构造函数,字段或者方法中。
- Dagger2会在编译时通过apt生成代码进行注入。
Dagger2添加步骤:
总结一下Dagger2添加步骤:
- step 1:添加android-apt, dagger 2, dagger2-compiler以及javax annotation到build.gradle.(注意他们不都是compile的形式)
- step 2:添加模块(module),ApplicationModule将会注入Application Context 到需要的类中。
- step 3:添加组件Component, Dagger2 将会为你创建的所有component生成代码。使用文件名Dagger(Component)的形式。Component可以拥有多个module。(比如DaggerTaskDetailComponent拥有TaskDetailPresenterModule模块)
- step 4: 继承android.app.Application类,并且在AndroidManifest.xml中声明使用的application类。在它的onCreate()方法中构建主要组件(application component)
- step 5: 添加注入方法(inject)到Component 接口中,你需要为每一个参与到依赖注入的类添加inject()方法。(注意在dagger2中:为父类注入的依赖并不会为子类注入依赖关系,为子类注入的依赖关系则可以为父类注入依赖关系)参考TaskDetailPresenter方法。
- step 6: 注入依赖,用inject,替换你新建对象实例的地方。把这些新建实例的地方移到Modules中并且添加@Provides标注。可以参考上面的 ApplicationModule.java,在使用@Inject,请确保调用Component.inject()方法。可以参考上面的TaskDetailActivity.
- step 7: (可选,推荐)将getApplicationComponent()移到父类中(一般是指BaseActivity)
参考实际项目,请使用命令“git clone
https://github.com/googlesamples/android-architecture.git” 将项目clone到本地,当前是master分支,需要使用“git checkout todo-mvp-dagger” 切换到todo-mvp-dagger分支。
使用建议
全局Application:主要实现一些单例
模块:如网络模块、数据库模块、复杂界面
参考资料
Google官方MVP+Dagger2架构详解【从零开始搭建android框架系列(6)】
本文基于这篇文章修改、补充说明。简单使用可以参考,相关步骤说明都简单易懂
使用Dagger 2进行依赖注入 - API介绍
从零开始的Android新项目4 - Dagger2篇 分析Dagger优势
Dagger2使用
详解Dagger2 加深理解
Dagger2使用攻略 结合Demo学习,分析生成的代码
参考Demo
googlesamples/android-architecture todo-mvp-dagger
分支
simplezhli/Dagger2Example