Dagger 2的基础使用方法

概述

许多安卓app中的对象经常需要用到其他的依赖库。比如,一个使用推特API的客户端需要用到网络库如Retrofit库。为了使用这些第三方库,你可能还需要添加数据解析库如Gson。另外,那些实现授权或者缓存的类还可能要访问shared preferences或者其他的储存,这些都需要各种依赖的初始化并形成一个依赖继承链。

Dagger 2能帮你分解依赖库并生成代码将它们整合在一块。现在网上已经有很多其他的Java依赖注入库,其中很多都要会受一些限制,或是依赖XML,或是在运行时性能上会受到拖累。Dagger 2只依赖Java的注释解析器和在编译时分解、确认依赖。到目前为止它被认为是最有效率的依赖注入框架之一。

使用Dagger 2的益处

以下是使用Dagger2的一些其他的益处:

  • 访问共享实例变得更简单。就像ButterKnife库让视图、事件处理和资源的引用定义变得简单一样。Dagger2提供了一种简单的方式来获取共享实例的引用。比如,一旦我们在Dagger 2中声明了单例对象如Retrofit,我们可以用一个简单的@Inject注释来声明变量。

  • 使复杂的依赖配置变得简单。Dagger 2可以自动完成依赖链和生成代码,并且两者都易读、易追踪,使你从繁杂重复的模板代码中解放出来。这还会使重构变得简单,因为你不用再将注意力放在依赖的生成顺序上。

  • 更加容易单元测试和集成测试。因为依赖链已经创建好了,所以我们很容易置换出模组来模拟网络响应或是各种行为。

  • 给实例加上作用域。你不仅可以让你的实例生命周期持续整个应用,你还可以通过Dagger 2将实例的生命周期变得更短(如activity生命周期或是用户session)。

基础使用

我们用Retrofit进行网络请求作为例子。假如不使用任何类型的依赖注入框架,每次进行网络请求时,我们可能都需要敲下类似下面的代码来进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
int cacheSize = 10 * 1024 * 1024;
Cache cache = new Cache(getApplication().getCacheDir(), cacheSize);
OkHttpClient okHttpClient = new OkHttpClient.Builder()
.cache(cache)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com")
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();

不过,就像前面所说,当我们使用Dagger 2时,这一过程会被简化并且使配置更加灵活。

后面会接触两个基本的概念ComponentModule,它们会在Dagger 2中被频繁用到。

如果将依赖注入视为一个打针的过程的话,那么Module就是针筒里的液体,而Component就是针筒。

创建module

我们可以用@Module注释一个类,这个类中用@Provides注释的方法,可以视为其返回类型的构造器。在Activity或Fragment中用Component注射后,被@Inject注释的变量会自动使用这些构造器初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Module
public class ApplicationModule {
private final Application application;
public ApplicationModule(Application application) {
this.application = application;
}
@Provides
Application providesApplication() {
return application;
}
}

下面的代码中用@Provides注释了一些列方法,返回类型分别是Cache、OkHttpClient、Retrofit。当这些方法被调用时,这些方法会在Module中寻找是否存在与方法参数类型一致的其他同样被@Provides注释的方法,如果存在,就会自动用那个方法作为参数的构造器。

以下面的代码举例,当返回Retroift的方法providesRetrofitWithCache被调用时,其需要的参数会自动调用providesOkHttpClientWithCache返回的对象,而providesOkHttpClientWithCache此时也需要参数即Cache,则又会调用providesCache返回的对象,最后providesCache又会需要Application类型的对象作为参数。但是这个参数目前NetModule中找不到,不过后面通过ComponentModule可以跨Module寻找参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@Module
public class NetModule {
private final String baseUrl;
public NetModule(String baseUrl) {
this.baseUrl = baseUrl;
}
@Provides
Cache providesCache(Application application) {
int cacheSize = 10 * 1024 * 1024;
return new Cache(application.getCacheDir(), cacheSize);
}
@Provides
OkHttpClient providesOkHttpClientWithCache(Cache cache) {
return new OkHttpClient.Builder()
.cache(cache)
.build();
}
@Provides
@Singleton
Retrofit providesRetrofitWithCache(OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
}
}

注意,用@Provides注释的方法的名字并不是必须以provides开头,这只是一种习惯,你可以用随意使用其他方法名。

@Singleton注释表示这个方法返回的对象在整个生命周期内置被初始化一次。

创建component

像前面作的比喻一样,在Dagger 2中Component被称为注射器类。通过它,activity、fragment、service类可以用Module中暴露的方法初始化参数。像Module需要用@Module注释一样,Component同样需要用@Component注释。@Component括号内的代码表示这个Component与两个Module即NetModule和ApplicationModule相关联,并且这两个Module也有了联系。这样它们就能够互相访问彼此内部用@Porivdes注释的方法,所以当NetModule类中providesCache在NetModule中找不到对应参数类型的其他方法时,还可以去ApplicationModule中搜索。

1
2
3
4
5
@Singleton
@Component(modules = {NetModule.class, ApplicationModule.class})
public interface NetComponent {
void inject(MainActivity mainActivity);
}

注意,用Component给activity、fragment、service注射前,必须在Component类中声明一个inject方法,并且参数类型必须是强类型,比如要给MainActivity注射的话,参数就必须是MainAcitivity类型。

这里的@Singleton目前只知道应该是一种对应的模式。如果在Moduel中有需要声明@Singleton的方法,那么包含这个ModuleComponent也需要被注释。

生成代码

ModuleComponent都配置完毕后,可以直接build一次项目,这样Dagger 2会自动生成需要用到的代码。如果没出错的话,每个Component会自动生成一个Dagger前缀的类,就像前面的NetComponent,会自动生成一个DaggerNetComponent类,利用这个类我们可以完成NetComponent的初始化。如果有兴趣可以了解一下android-apt插件,它会让你更好的理解Dagger 2生成的代码。

实例化component并注入

我们可以在Application中初始化一次,然后通过get方法获取引用,这样就不必重复工作了,这样Component的生命周期会持续整个Application。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyApplication extends Application {
private NetComponent netComponent;
@Override
public void onCreate() {
super.onCreate();
netComponent = DaggerNetComponent.builder()
.applicationModule(new ApplicationModule(this))
.netModule(new NetModule("https://api.github.com"))
.build();
}
public NetComponent getNetComponent() {
return netComponent;
}
}

因为自定义了Application类,还需要在AndroidManifest中声明一下。

最后开始给MainActivity注入依赖。在onCreate方法中调用Application中的get方法获取NetComponent的引用并调用inject方法。

@Inject表示我们需要注入的内容,此处标识了一个Retrofit变量,这样Dagger 2就会自动在Module中调用对应类型的用@Provides注释了的方法作为构造器,并且构造器需要的参数会像之前说过的一样在Module中搜索。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MainActivity extends AppCompatActivity {
@Inject
Retrofit retrofit;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
((MyApplication)getApplication()).getNetComponent()
.inject(this);
}
}

限定符注释

当我们需要在Module中定义一个相同返回类型但是配置不一样的方法时,我们可以用@Named注释进行标识。你可以在@Inject标识的变量前用@Named标识,可以在@Provides标识的方法前用@Named标识,还可以在@Provides标识的方法的参数前用@Named进行标识。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
@Module
public class NetModule {
private final String baseUrl;
public NetModule(String baseUrl) {
this.baseUrl = baseUrl;
}
@Provides
Cache providesCache(Application application) {
int cacheSize = 10 * 1024 * 1024;
return new Cache(application.getCacheDir(), cacheSize);
}
@Provides
@Named("cache")
OkHttpClient providesOkHttpClientWithCache(Cache cache) {
return new OkHttpClient.Builder()
.cache(cache)
.build();
}
@Provides
@Named("no cache")
OkHttpClient providesOkHttpClient() {
return new OkHttpClient.Builder()
.build();
}
@Provides
@Named("cache")
@Singleton
Retrofit providesRetrofitWithCache(@Named("cache") OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
}
@Provides
@Named("no cache")
Retrofit providesRetrofit(@Named("no cache") OkHttpClient okHttpClient) {
return new Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build();
}
}

在注入是@Named括号内的名字要求一致。

1
2
3
@Inject
@Named("cache")
Retrofit retrofit;

@Named只是Dagger 2提前定义的一个@Qualifier类型的注释,你可以自定义需要的注释:

1
2
3
4
5
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface DefaultPreferences {
}

作用域注释

在Dagger 2中,你可以自定义作用域注释来控制Component的生命周期。比如你可以将作用域控制在整个Application内,也可以控制在一个activity内,甚至控制在一个user session内。自定义方法类似限定符注释:

1
2
3
4
5
@Scope
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface MyActivityScope {
}

实际上个人认为这个作用域注释只是个类似普通注释一样的东西,它唯一的作用就是在Component的作用域内,用它注释的@Provides方法返回的实例是单例。它的名字只是告诉程序员,这个Component的作用域的范围。

拿前面的@Singleton举个例子,在MainActivity内部获取的NetComponent是在Application中初始化的,如果我们再创建一个SecondActivity,并且同样获取Application中的NetComponent引用并注入,这两个Activity中的Retrofit引用的会是同一个对象。因为此时的NetComponent的生命周期为整个应用。当我们将@Singleton换成其他名字的作用域注释(比如@NotSingleton)时,它起到的作用会是一样的。

区别只存在于是否有作用域注释。当去掉NetComponent和Module中的@Singleton时,MainActivity和SecondActivity中的Retrofit对象就会引用不同的对象了。

Demo项目地址

Demo

这个项目有多个分支,与本文相关的只有master分支。

参考文献