Dagger2 菜鸟入门

如果你对 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'
    }
}

appbuild.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 只提供了两点编译期的检查:

  1. 一个 Module 里只能存在一种 Scope
  2. 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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容