如果你对 Dagger2 和 依赖注入 都完全没有概念,也没关系,这篇文章会从最简单的概念开始,教你如何上手 Dagger2
从 Dagger1 到 Dagger2, 这个依赖注入框架已经火了很久了。然而其涉及的一些概念不是那么容易理解,导致其不是很好上手。在参考了多篇大牛的文章(见文末)后,我总算基本弄懂了这个框架,所以赶紧记录下来。如果有理解出现偏差的地方,恳请及时的指出。
0. Dagger2 和 依赖注入
这里不摆长篇大论的定义,只用最简单的方法来说清楚基本概念
Dagger2 是什么?
Dagger2 是一个依赖注入框架
什么是依赖注入?
当 A 类中包含一个属性 B 类,就可以说 A 对 B 产生了 依赖。
当实例化一个对象的时候,不再需要 new 出一个实例,而是由框架自动的帮你生成一个实例,这就是 注入。
为了实现自动化的注入,需要前期的一些配置工作,后面详细说。
依赖注入到底有什么好处?
简单的说,就是 将类的初始化以及类之间的依赖关系集中在一处处理。你修改了一个类的构造方法,那就要在所有调用该构造函数的地方修改。如果使用了依赖注入框架,类的构造都集中在一处,可以很方便的进行更改而不影响其他部分的代码。
依赖注入的优势在较小的项目中体现的不明显,甚至会因为比较复杂的配置,以及反直觉的对象初始化方式而让人望而却步。但是在复杂的项目中,有大量类的初始化,以及类之间的依赖关系很复杂的时候,依赖注入就会变的十分必要。其实工厂方法也是为了解决同样的问题,只不过要写大量的 boilerplate code,而 Dagger2 只需要进行一些配置之后,就可以在编译后自动的生成代码,相比工厂方法更加智能。
1. 在 Android Studio 中配置 Dagger2
Dagger2 是一个通用的 java 库,并不只适用于 Android,这里仅以 Studio 下的 Android 开发示例。
在项目的根 build.gradle
里添加:
buildscript {
...
dependencies {
...
//编译时处理注解的插件,生成代码就靠它
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
在 app
的 build.gradle
里添加:
apply plugin : 'com.neenbedankt.android-apt'
...
dependecies{
...
//dagger2 依赖库
compile 'com.google.dagger:dagger:2.2'
provided 'com.google.dagger:dagger-compiler:2.2'
//dagger2 中用到几个 java sdk 里的注解 @Inject,@Singleton
provided 'org.glassfish:javax.annotation:10.0-b28'
}
2. Dagger2 中的注解
Dagger2 使用注解来配置对象之间的依赖关系。可以把对象之间的互相依赖想象成一个有向无环图,而在代码中使用这些注解,就是为了来描述这个图。其实理论上,描述依赖关系的方法还可以有很多,比如用一个配置文件来描述(Spring 框架),只不过直接在代码中用注解,更加的直接清晰。
理解 Dagger2 中的几个注解的含义和用法,是掌握 Dagger2 的关键,下面一个一个的分析。
2.1 @Module
注解一个类,这个类用来提供依赖,或者通俗的说,完成各种对象的实例化 的一个集合。类里面可以包含很多 @Provides
注解的方法,每个方法对应一种对象的创建。
2.2 @Provides
注解 Module 类里面的方法,Dagger 会在需要创建实例的时候找到这个方法并调用,完成对象的实例化。
这样说可能还是太抽象,看一个实例:
@Module
public class AppModule {
private final MyApplication mApplication;
public AppModule(MyApplication application) {
this.mApplication = application;
}
@Provides @Singleton
MyApplication provideApplication() {
return mApplication;
}
}
去掉注解,AppModule
就是一个普通的类,@Module
注解的含义很简单,它就是告诉 Dagger 框架,这个类里面有可以提供对象创建的方法。所以 Module 类可以理解成一个用来组织对象创建方法的容器,关键是其内部的方法。
2.3 @Inject
当注解一个属性的时候,表示该属性需要依赖(需要被注入一个对象)。
当注解一个构造函数的时候,表示该构造函数可以提供依赖。需要注意的是,如果被 @Inject
注解的构造函数是带参数的,比如这样:
public class AClass{
@Inject
public AClass(BClass bClass){
//do something
}
}
那么 Dagger 框架会在编译期检查, 是否在 Module类 中有返回值是 BClass 的方法,或者 BClass 是否有被 @Inject
注解的构造函数。如果未能找到,就会在编译期报错。这就在编译期确保了对象之间的依赖一定会被满足
2.4 @Component
前面说了 @Module
提供依赖, @Inject
请求依赖,而@Component
就是联系这两者的纽带。
Component 主要说明4件事:
- 谁来提供依赖
- 该 Component 依赖哪些其他的 Component
- 该 Component 为谁提供依赖注入
- 该 Component 可以提供那些依赖
@Component
注解的是一个接口,比如下面这个接口,表示它可以提供一个 MyApplication 类型的依赖。
@Singleton
@Component(modules = {AppModule.class},dependencies = {xxxComponent.class})
public interface AppComponent{
void inject(MyActivity myActivity);
public MyApplication getApplication();
}
对应前面说的4点:
-
module
参数{}里的类就表示提供依赖的Module类(可以有多个),也就是用@Module
注解的类 -
dependencies
表示该 Component 提供的注入类的构造函数中,还依赖其他@Component
提供的一些类。 - 有参无反的方法指明该 Component 注入的目标,比如本例中,就说明 MyActivity 中需要 AppComponent 提供的依赖
- 有反无参的方法指明该 Component 可以提供哪些依赖,或者说暴露哪些依赖。因为这里可能会有疑问,Component 可提供的依赖不就是 Module 里的那些吗?确实没错,但是 Component 也可以有选择的只对外界暴露它的一部分能力,并不一定会声明所有的在 Module 里定义的类。
这个接口可以理解成一份声明,告诉Dagger哪些对象可以被注入,以及谁(Module类中被@Provides
注解的方法)来提供这些依赖。需要注意,只有在 Component 接口中声明了的类,才会被暴露给依赖它的其他 Component。也就是说,Module 类中提供的依赖,并不一定都会在 Component 中声明。
最后,这个接口的实现会由 Dagger 框架自动生成,生成类的名字满足 Dagger + 你的接口名
的格式。可以在 项目的 app-buile
目录下找到生成的代码。后面的使用也主要是跟 Component 的实现类打交道。
2.5 @Named
有时候我们需要生成同一个类的两个不同的实例,这时候就要用到 @Named
。它的用法很简单,直接看代码:
@Provides
@Named(“default”)
SharedPreferences provideDefaultSharedPrefs() { … }
@Provides
@Named(“secret”)
SharedPreferences provideSecretSharedPrefs() { … }
提供两种不同的 SharedPreferences
依赖,在需要注入的地方这样标记:
@Inject @Named(“default”)
SharedPreferences mDefaultSharedPrefs;
@Inject @Named(“secret”)
SharedPreferences mSecretSharedPrefs;
含义应该不言自明了吧,概括一下就是根据 @Named
后面字符串来匹配需要注入哪种实例
2.6 @Singleton & @Scope
你可能注意到前面的 Module 类和 Component 接口都出现了 @Singleton
,它代表了 Dagger 框架里一种叫做 Scope
的概念。这个概念主要是为了解决不同的对象生命周期不同的问题。有一些对象比如 Application,DBManager 等存在于应用的整个周期,而像 Adapter,Presenter 等对象则随着 Activity 的销毁而死去,还有一些比如像数据库连接,会随着用户的切换而创建和销毁。
除了@Singleton,还可以自定义自己的 Scope:
@Scope
public @interface MyScope{}
然后你可以把前面例子中出现 @Singleton 的地方都换成 @MyScope,效果是一样的。
不要被 @Singleton
的表面含义所迷惑,而认为只有用 @Singleton
注解的 Component 提供的依赖才是单例,自定义的 Scope 就不是单例。实际上,只要加了 Scope 注解的 Component 提供的依赖都是单例,不加 Scope 注解的就是每次注入的都是新的实例。这里一定要明确一个概念,那就是
Dagger 框架不会帮你管理对象的生命周期,需要自己来控制!!!
具体的说来,如果你需要一个对象的随着用户的切换而改变,那么就在注销用户的时候销毁对应的 Component 及注入的对象,在登录用户的时候生成 Component 并进行对象的注入。
这是我一直很难理解 Scope
概念的一个主要误区。自定义的 Scope 标记只是起到一个标识作用,让 Coder 不要忘了按照对象生命周期的不同来组织依赖。 Dagger 只提供了两点编译期的检查:
- 一个 Module 里只能存在一种 Scope
- Scope 标记的 Component 跟其所依赖的 Component 的 Scope 不能相同
如果一时半会儿理解不了,没关系,先记下来,以后在使用中再慢慢理解为什么要有这样的设定。
3 Dagger 上手 N 步走
上面讲了这么多概念,可能都看懂了,但是到自己实际上手的时候,还是不知道该怎么使用。下面用一个例子来说明如何在项目中应用 Dagger2 框架实现依赖注入。
这里虚构一个场景,假设在 MyActivity 需要一个建立一个数据库连接 DBSession 去读取数据。(实际应用中使用 MVP 架构的话不会在 Activity 中直接进行数据读取操作,这里简单起见,暂不考虑架构问题)为了说明 Component 间的依赖,我们假定 DBSession 的构造方法依赖一个 User 对象。
3.1 确定需要依赖的类
这个例子中很简单, MyActivity 依赖 DBSession,所以在 MyActivity 标记:
public class MyActivity{
@Inject
DBSession dbSession;
...
}
3.2 定义Module
定义 Module 类,提供对象依赖。此时也要明确对象的生命周期,我们这里就假定 DBSession 的生命周期跟用户的切换相关,而 User 的生命周期跟应用一样。这里我们构造两个 Module:
@Module
public class DBModule{
@Provides @Peruser
DBSession provideDBSession(User user){ return new DBSession(user); }
}
@Module
public class UserModule{
@Provides @Singleton
User provideUser(){ return new User(); }
}
3.3 定义 Component
Module 定义好之后,就需要 Component 来把他们之间的依赖关系连接起来了。这里把 Component 依赖以及 Scope 的概念都应用上了
@Singleton
@Component(modules = {UserModule.class})
public interface UserComponent{
public User getUser();
}
@PerUser
@Component(modules = {DBModule.class}, dependencies = {UserComponent.class})
public interface DBComponent{
void inject(MyActivity myActivity);
public DBSession getDBSession();
}
这里注意,因为 UserComponent 不直接为其他对象注入依赖,所以里面就不需要定义 void inject(Target target)
方法
3.4 初始化 Component
经过上面三步之后,编译一下项目,会生成 DaggerUserComponent 和 DaggerDBComponent 两个实现类。初始化 Component 的代码如下:
userComponent = DaggerUserComponent.builder()
.userModule(new UserModule())
.build();
dbComponent = DaggerDBComponent.builder()
.userComponent(userComponent)
.dbModule(new DBModule())
.build();
这里有几点需要说明:
- DBComponent 依赖 UserComponent,所以在初始化的时候要传入一个 UserComponent 对象
- 如果 Module 没有定义构造函数,也就是只有隐含的无参构造函数的话,那么可以省掉
.userModule(new UserModule())
和.dbModule(new DBModule())
这两句。但如果定义了有参的构造函数,则要么再自己定义一个无参构造函数,要么就不能省去创建 Module 的这句了。至于原因,去看生成的 Component 实现类就知道了。 - Component 初始化的地方,以及何时初始化,这个需要自己来控制,这里写在一起不代表实际中它们都会在一起初始化,可能有的在 Application 里初始化,有的在 Activity 里初始化,有的在满足一定条件下初始化。说到底,是跟其 Scope ,也就是生命周期有关。
3.5 最后一步注入
万里长征终于要结束了,前面铺垫了那么多,就为了最后这一句
public class MyActivity{
@Inject
DBSession dbSession;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
//这里假设 dbComponent 已经完成初始化
dbComponent.inject(this);
}
}
在 inject 之后,依赖于这个 Component 的对象就都完成了实例化的动作了。再也见不到满屏的 new 方法了!
4 未完待续
不得不说,这应该是我接触过的最难上手的库了,难就难在你就算看懂了每个注解的含义,但是到自己写的时候还是感觉无从下手。希望看完这篇之后,你能知道如何开始在项目中用 Dagger2 进行一些简单的依赖注入。
其实还有一些概念没有讲,比如 SubComponent ,一方面是因为多数情况下用 dependencies 就够了,另一方面是我自己也还没有完全搞懂,所以等到更多的应用之后,再来写一篇关于 Dagger2 应用中需要注意的问题的文章吧。
参考文献
1. Tasting Dagger 2 on Android
2. Dagger 2: Even sharper, less square
3. Dependency injection with Dagger 2 - Custom scopes
4. Making a Best Practice App #4 — Dagger 2