Earth Guardian

You are not LATE!You are not EARLY!

0%

Dagger2

基本概念

Dagger2 依赖注入框架,用来解耦类关系。特别是 MVP 分层结构中,方便解耦和分层测试
A fast dependency injector for Android and Java.
仓库地址:https://github.com/google/dagger

依赖

  • build.gradle
1
2
3
4
5
6
dependencies {
...
// Better IDE support for annotations (so Android Studio interacts better with Dagger)
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
...
}
  • app/build.gradle
1
2
3
4
5
6
7
8
9
10
apply plugin: 'com.neenbedankt.android-apt'

dependencies {
...
// Dagger dependencies
apt "com.google.dagger:dagger-compiler:2.11"
provided 'org.glassfish:javax.annotation:10.0-b28'
compile "com.google.dagger:dagger:2.11"
...
}

注解关键字

Inject

  • 标注变量
    要求 Dagger 为该变量提供依赖,实例化该变量。不能用 private 修饰符修饰该变量
  • 标注构造函数
    @Inject 标记了的变量在需要这个类实例的时候,Dagger 会找到这个构造函数并将其实例化。也就是 @Inject 标记的构造函数为 @Inject 标记的变量提供依赖(实例化)。构造函数的参数不需要使用 NonNull 标注,Dagger 会做强制检测
  • 标注方法
    标注的方法会自动在构造函数完成实例化后调用

Module

格式:@Module(includes = {*module.class})
@Module 注释类,用于标注提供依赖的类。和被 @Inject 标记的构造函数功能类似。但是有时候提供依赖的构造函数是第三方库,无法通过 @Inject 来标注;而且如果构造函数带参数,@Inject 也无法提供参数,所以需要 @Module 来标注。Module 类是一个简单工厂模式,通过 @Provider 提供三个重要功能:

  • @Inject 变量提供实例
  • @Inject 构造函数提供参数
  • 给其他 @Module 中的 @Provider 方法提供参数

@Inject(构造函数)和 @Module 都可以提供依赖(实例化), Dagger2 会如何选择呢?具体规则如下:

1
2
3
4
5
6
7
步骤1:首先查找 @Module 标注的类中是否存在提供依赖的方法 @Provider。
步骤2:若存在提供依赖的方法(实例化),查看该方法是否存在参数。
a:若存在参数,则按从步骤1开始依次初始化每个参数(开始查找参数的依赖关系,即参数的实例化是否存在依赖注入);
b:若不存在,则直接初始化该类实例,完成一次依赖注入。
步骤3:若不存在提供依赖的方法,则查找 @Inject 标注的构造函数,看构造函数是否存在参数。
a:若存在参数,则从步骤1开始依次初始化每一个参数
b:若不存在,则直接初始化该类实例,完成一次依赖注入。

所以:创建类实例级别 @Module 维度要高于 @Inject 维度

Provider

注释方法,以 provide 作为前缀,必须出现在有 @Module 注释的类中

它是根据返回值类型来标识和匹配的,方法名并不重要,只需要保证以 provide 开头方便阅读即可

这里细化下 @Module 通过 @Provider 提供的三个功能:

  • @Inject 变量提供实例
    也就是将实例化放到了 Module
1
2
3
4
5
6
7
8
9
@Module
public class TasksRepositoryModule {

@Singleton
@Provides
public TasksDataSource provideTasksDataSource(Context context){
return new TasksDataSource(context);
}
}
  • @Inject 构造函数提供参数
1
2
3
4
5
6
7
8
// 构造函数及所需参数
@Inject
public TaskDetailPresenter(long taskID, TasksDataSource
tasksDataSource, TaskDetailContract.View taskDetailView) {
mTaskID = taskID;
mTasksDataSource = tasksDataSource;
mTaskDetailView = taskDetailView;
}

对应类构造函数所需参数,可以通过 Module 的构造函数,将参数传递进来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 对应 module 代码  
@Module
public class TaskDetailPresenterModule {

private final TaskDetailContract.View mTaskDetailView;
private long mTaskID = 0;

// 提供构造函数的两个参数,通过实例化 Module 的方式传递进来
public TaskDetailPresenterModule(TaskDetailContract.View view, long taskID) {
mTaskDetailView = view;
mTaskID = taskID;
}

@Provides
public TaskDetailContract.View provideTaskDetailContractView(){
return mTaskDetailView;
}

@Provides
public long provideTaskID(){
return mTaskID;
}
}
  • 给其他 @Module 中的 @Provider 方法提供参数
    该方式需要通过后面介绍的 @Component 将多个 @Module 类连接起来才能生效。给其他 @Module 中的 @Provider 方法提供参数依赖(实例化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Module
public class TasksRepositoryModule {
@Singleton
@Provides
public TasksDataSource provideTasksDataSource(Context context){
return new TasksDataSource(context);
}
}

// ApplicationModule 为 TasksRepositoryModule 的 @Provider 提供参数 context
// 而 ApplicationModule 的 context 来自于自身构造函数的传参
// 通过 @Component 来连接两个 @Module
@Module
public class ApplicationModule {
private Context context;
public ApplicationModule(Context context){
context = context;
}
@Provides
Context providerContext(){
return context;
}
}

Binds

用来简化 @Provider 的注解

  • 被注解的方法必须是抽象
  • 必须指定返回类型,也就是类似 @Provider 的返回值

示例:将 Random 绑定到 SecureRandom 上:

1
2
3
4
5
6
7
8
@Binds
abstract Random bindRandom(SecureRandom secureRandom);

简化如下方案:
@Provider
Random bindRandom(SecureRandom secureRandom){
return secureRandom;
}

Component

接口或抽象类,是依赖需求方 (@Inject) 和依赖提供方 (@Module) 之间的桥梁。在编译过程中 Dagger 会自动生成对应的实现类 Dagger***Component

如果 @Component 标记的类不是顶级类,生成的实现类格式为 DaggerA_B_function ,类结构之间增加下划线

注解格式

格式:@Component(dependencies = {*component.class}, modules = {*modules.class})
参数 dependencies :表示 ComponentComponent 之间的依赖关系
参数 module :表示依赖提供方
示例:

1
2
3
4
5
6
@FragmentScoped
@Component (dependencies = TasksRepositoryComponent.class,
modules = TaskDetailPresenterModule.class)
public interface TaskDetailComponent {
...
}

@Component 注解的接口或抽象类

必须包含 Provision methods 或者 Members-injection methods 其中的一种方法

1
2
3
4
5
6
7
8
9
10
11
12
@Singleton
@Component(modules = DataRepositoryModule.class)
public interface AppComponent {
// Provision methods,使用 @Inject 注解了 DataRepository 变量
DataRepository getDataRepository();
@Component.Builder
interface Builder{
@BindsInstance
AppComponent.Builder providerContext(Context context);
AppComponent build();
}
}

Provision methods:该方法没有参数,但是返回值必须为 @Inject 注解变量的类型或者注解过的构造函数的类(也就是依赖需求方),或者为 @Provider 返回值类型:DataRepository getDataRepository();

Members-injection methods:该方法满足下面条件之一:

  • 返回值为 void 或者 传入的参数类型,此时方法必须且只能包含一个参数,该参数为 @Inject 注解所在类的类型
1
2
3
// @Inject 在 TaskDetailActivity 中
void inject(TaskDetailActivity activity);
SomeType injectAndReturnSomeType(SomeType someType);
  • 返回值为 MembersInjector 类型,可以没有参数
    MembersInjector<SomeType> getSomeTypeMembersInjector();

获取 Component 实例

格式:Dagger***Component.builder().***初始化参数***.build(),举例:

1
2
3
mTasksRepositoryComponent = DaggerTasksRepositoryComponent.builder()
.applicationModule(new ApplicationModule(getApplicationContext()))
.build();

@Component 在实例化时,会将对应的所有 @Module 全都实例化,此时可以通过 @Module 构造函数传入参数

@Component.Builder

默认是不需要重新定义的。如果需要通过 @Component 将参数传递给 @Module@Provider 中,我们可以在这里定义 @BindsInstance

1
2
3
4
5
6
7
@Component.Builder
interface Builder{
// 传递 Context
@BindsInstance
AppComponent.Builder providerContext(Context context);
AppComponent build();
}

需要注意 @Component.Builder 需要满足几点条件,具体参考 Dagger 的文档。其中必须有一个 build 返回值为 @Component 类, 每次只能传递一个参数并且返回值为 Builder 类型。

Scope

用于自定义的注解,限定注解作用域,实现局部的单例

Singleton

注解表示为单例模式。但是 Dagger 并没有真正的创建单例,需要我们手动写代码确保只实例化一次。@Singleton 需要同时标记 ProviderComponent,并且我们把实例化该 Component 放到 Application.onCreate 中,确保只会实例化一次,这样才能体现出单例模式的效果。作用:

  • 更好的管理 ComponentModule 之间的关系,保证它们是匹配的。如果两者的 Scope 是不一样的,则在编译时报错
  • 代码可读性,更好的了解 Module 中创建的类实例是单例

Qulifier

用于自定义注解,精确指定 @ModuleInject 的依赖对应关系。
@Module 来标注提供依赖的方法时,方法名可以随便定义的,但是为了增加可读性,一般以 provide 开头。Dagger2 主要是根据返回值的类型来决定为哪个被 @Inject 标记了的变量赋值。但是如果有多个一样的返回类型,就无法区分了。因此使用 @Qulifier 来自定义一个注解,然后通过自定义的注解去标注提供依赖的方法和依赖需求方,这样就能精确匹配了。
一个更为精简的定义:当类型不足以鉴别一个依赖的时候,我们就可以使用@Qulifier 这个注解标示。

常规步骤

引入依赖文件

build.gradleapp/build.gradle 中添加依赖,并选择 Sync Project 同步并下载这些第三方包

Inject 标注

标注需要依赖的变量,或者提供依赖的构造函数

实现 @Module 和 @Provider

根据 Inject 来设计 Module

  • Inject 标注了构造函数
    Module 可以提供构造函数所需参数
  • Inject 没有标注构造函数
    Module 直接实例化并提供返回值

实现 @Component

  • 指定范围 Scoped
  • 选定依赖和指定对应的 Module
  • @Inject 标注的依赖变量,如果需要实例化,需要将变量所在类传递给 Component
    void inject(TaskDetailActivity activity);

编译项目

Dagger 自动生成 ModuleComponent 对应的文件

关联注入

Dagger***Component 实现注入并实例化 @Inject 标注的变量

注意

如果觉得 Dagger 生成的不对,可以 clean 整个工程,重新生成一次。有的时候只修改 @Inject 注解,并不会正确的生成对应代码,需要 clean & remake 才行

常见错误

没有提供依赖实例化方法

@Inject 注解成员后,没有注解构造函数,导致如下错误:

1
2
3
4
5
6
7
8
9
10
C:\Users\xmt\AndroidStudioProjects\xmt\gitlab\02_myTodo\myTodo\app\src\main\java\com\ymzs\todo\mytodo\ToDoApplication.java
Error:(5, 33) 错误: 找不到符号
符号: 类 DaggerTaskRepositoryComponent
位置: 程序包 com.***.mytodo.data
C:\Users\xmt\AndroidStudioProjects\xmt\gitlab\02_myTodo\myTodo\app\src\main\java\com\ymzs\todo\mytodo\tasks\TasksComponent.java
Error:(13, 10) 错误: com.***.mytodo.tasks.TasksPresenter cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.
com.***.mytodo.tasks.TasksPresenter is injected at
com.***.mytodo.tasks.TasksActivity.mTasksPresenter
com.***.mytodo.tasks.TasksActivity is injected at
com.***.mytodo.tasks.TasksComponent.inject(activity)

@Component 没有定义范围

没有定义 scoped@Component 不能依赖一个定义了 scoped@Component,否则导致如下报错:

1
2
Error:(13, 1) 错误: com.***.mytodo.tasks.TasksPresenterComponent (unscoped) cannot depend on scoped components:
@Singleton com.***.mytodo.data.TasksRepositoryComponent

@Inject 标注私有字段

@Inject 不支持注解私有字段,否则会导致如下错误:

1
2
3
4
5
6
Error:(5, 33) 错误: 找不到符号
符号: 类 DaggerTasksRepositoryComponent
位置: 程序包 com.***.mytodo.data
C:\Users\xmt\AndroidStudioProjects\xmt\gitlab\02_myTodo\myTodo\app\src\main\java\com\ymzs\todo\mytodo\tasks\TasksActivity.java
Error:(27, 28) 错误: Dagger does not support injection into private fields
C:\Users\xmt\AndroidStudioProjects\

@Binds 注解的方法,没有提供输入参数

Error:(18, 20) 错误: android.app.Application cannot be provided without an @Inject constructor or from an @Provides-annotated method.
@Binds 注解的方法,必须要提供一个 @Inject 构造函数或者一个 @Provides 注解的方法:

1
2
@Binds
abstract Context bindContext(Application application);

@Component.Builder 必须要有一个无参方法返回 @Component 类型

Error:(21, 5) 错误: @Component.Builder types must have exactly one no-args method that returns the @Component type
必须要有一个无参方法返回 @Component 类型,这个可以理解为 build() 提供该 Component 的实例

1
2
3
4
5
@Component.Builder
interface Builder{
// 必须包含一个无参方法
AppComponent build();
}

参考文档

  1. 官网文档
  2. Android:dagger2基础注入框架
  3. 详解Dagger2
  4. 神兵利器Dagger2