这章是展示使用Dagger 2在Android端实现依赖注入的系列中的一部分。今天我会探索Dagger 2的基础并且学习这个依赖注入框架的所有的API。
Dagger 2
在我上一章中我提到了DI框架给我们带来了什么 - 它让只写一小部分代码就可以使一切联系在一起成为可能。Dagger 2就是DI框架的一个例子,它可以为我们生成很多模版代码。但是为什么它比其它的要更好呢?目前为止它是唯一一个可以生成完整模仿用户手写的可追踪的代码的DI框架。这意味着让依赖图表构建过程不再充满魔幻。Dagger 2相比其它是不具有动态性的(使用时完全不使用反射)但是生成的代码的简洁性和性能都是与手写的代码同水准的。
Dagger 2 基础
下面是Dagger 2的API:
public @interface Component {
Class<?>[] modules() default {};
Class<?>[] dependencies() default {};
}
@interface Subcomponent {
Class<?>[] modules() @interface Module {
Class<?>[] includes() @interface Provides {
}
@interface MapKey {
boolean unwrapValue() default true;
}
public interface Lazy<T> {
T get();
}
还有在Dagger 2中用到的定义在JSR-330(Java中依赖注入的标准)中的其它元素:
让我们来学习它们。
@Inject 注解
DI中第一个并且是最重要的就是@Inject
注解。JSR-330标准中的一部分,标记那些应该被依赖注入框架提供的依赖。在Dagger 2中有3种不同的方式来提供依赖:
构造器注入:
@Inject
使用在类的构造器上:
所有的参数都是通过依赖图表中获取。@Inject
注解被使用在构造器中也会标记这个类为依赖图表的一部分。这意味着它也可以在我们需要的时候被注入:
这个例子中的局限性是我们不能给这个类中的多个构造器作@Inject
注解。
属性注入
另一种选择是给指定的属性作@Inject
注解:
但是在这个例子中,注入过程必须要在我们类的某处“手动”调用:
在这个调用之前,我们的依赖是null值。
属性注入的局限性是我们不能使用private
。为什么?简单说,生成的代码会直接地调用它们来设置属性,就像这里:
//This class is generated automatically by Dagger 2
public final SplashActivity_MembersInjector implements MembersInjector<SplashActivity> {
//...
public injectMembers(SplashActivity splashActivity) {
if (splashActivity == null) {
throw new NullPointerException("Cannot inject members into a null reference");
}
supertypeInjector.injectMembers(splashActivity);
splashActivity.presenter = presenterProvider.get();
splashActivity.analyticsManager = analyticsManagerProvider.get();
}
}
方法注入
最后一种方法使用@Inject
注解提供依赖的方式是在这个类的public
方法中作注解:
方法的所有参数都是通过依赖图表提供的。但是为什么我们需要方法注入呢?在某些情况下会用到,如当我们希望传入类的当前实例(this
引用)到被注入的依赖中。方法注入会在构造器调用后马上被调用,所以这表示我们可以传入完全被构造的this
。
@Module 注解
@Module是Dagger 2 API的一部分。这个注解用于标记提供依赖的类 - 多亏它Dagger才会知道某些地方需要的对象被构建。
@Module
GithubApiModule {
@Provides
@Singleton
OkHttpClient provideOkHttpClient() {
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setConnectTimeout(60 * 1000,TimeUnit.MILLISECONDS);
okHttpClient.setReadTimeout(60 * 1000,TimeUnit.MILLISECONDS);
return okHttpClient;
}
@Singleton
RestAdapter provideRestAdapter(Application application,OkHttpClient okHttpClient) {
RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setClient(new OkClient(okHttpClient))
.setEndpoint(application.getString(R.string.endpoint));
return builder.build();
}
}
view rawGithubApiModule.java hosted with ❤ by GitHub
@Provides 注解
这个注解用在@Module
类中。@Provides
会标记Module中那些返回依赖的方法。
@Component 注解
这个注解用在把一切联系在一起的接口上面。在这里我们可以定义从哪些module(或者哪些Components)中获取依赖。这里也可以用来定义哪些图表依赖应该公开可见(可以被注入)和哪里我们的component可以注入对象。@Component
通常是@Module
和@Inject
之间的桥梁。
这里有个使用了两个module的@Component
例子代码,可以注入依赖到GithubClientApplication
,并且标记了三个依赖公开可见:
@Component也可以依赖其它的component,并且定义了生命周期(我会在以后的文章中讲解Scope):
@Scope 注解
JSR-330标准的又一部分。在Dagger 2中,@Scope
被用于标记自定义的scope注解。简单说它们可以类似单例地标记依赖。被作注解的依赖会变成单例,但是这会与component的生命周期(不是整个应用)关联。但是正如我刚才所说 - 我会在下一篇文章中深入scope。现在值得一提的是所有自定义scope做的是同样的事情(从代码角度)- 它们为对象保持单例。但是他们也会被在图表认证处理中使用,这对尽可能地捕捉图表结构问题是有帮助的。
接下来是一些不太重要,不会经常用到的东西:
MapKey
这个注解用在定义一些依赖集合(目前为止,Maps和Sets)。让例子代码自己来解释吧:
定义:
提供依赖:
使用:
@MapKey注解目前只提供两种类型 - String和Enum。
@Qualifier
@Qualifier注解帮助我们去为相同接口的依赖创建“tags”。想象下你需要提供两个RestAdapter
对象 - 一个用于Github API,另一个用于Fackbook API。Qualifier
会帮助你区分对应的一个:
命名依赖:
注入依赖:
以上就是全部了。我们刚刚已经了解了所有Dagger 2 API中重要的元素了。
App example
现在是时候让我们在实践中检验我们的知识了。我们会基于Dagger 2来实现一个简单的Github客户端app。
想法
我们的Github客户端有三个Activity和非常简单的使用case。它的全部流程:
这里就是我们的app看起来的样子:
在内部,从DI角度我们的app结构看起来如下:
简单说 - 每一个Activity都有它自己的依赖图表。每个图表(_Component类)有两个对象 -_Prresenter
和_Activity
。每个component也会从全局的component(AppComponent
,包含了Application
,UserManager
,204)!important">RepositoriesManager等)中获取依赖。
讲讲AppComponent
- 看上面最近的图表。它包含了两个modules:AppModule
和GithubApiModule
。
GithubApiModule提供了一些依赖,比如OkHttpClient
或RestAdapter
,他们只会在这个module的其它依赖使用。在Dagger 2中我们可以控制哪些对象对外部的component可见。在我们例子中我们不希望暴露这些被使用了的对象。所以我们只是暴露了UserManager
和RepositoriesManager
,因为这有这些对象才会被我们的Activity用到。所有都通过public、返回非void类型并无参数的方法被定义。
文档中的例子:
提供依赖的方法:
SomeType getSomeType();
Set<SomeType> getSomeTypes();
@PortNumber int getPortNumber();
此外,我们必须要定义哪里我们希望要去注入依赖(通过成员注入)。在我们例子中AppComponent
没有任何地方可以去注入,因为它只是作为我们Components的依赖。所以它们每一个都被定义了 一个inject(_Activity activity)
方法。这里我们也有一些简单的规则 - 注入通过一个单个参数的方法被定义(定义一个实例,它代表我们需要往这个实例中注入依赖),它可以有任意的名字,但是必须要返回类型是void或者返回被传入的参数类型。
成员注入的方法:
实现
我不想深入太多的代码。cloneGithubClient代码并导入到最新的Android Studio。这里给出一些开始的提示:
Dagger 2 安装
只需要获得/app/build.gradle
文件。我们需要增加Dagger 2依赖和使用android-apt插件来对生成的代码和Android Studio IDE之间进行绑定。
AppComponent
从GithubClientApplication
类开始探索。在这里AppComponent
会被创建和存储。这意味着所有单例的对象的生命周期都会与Applicaiton一致(所以每时每刻都是如此)。
AppComponent的实现由Dagger 2(对象可以通过builder模式被创建)生成的代码提供。在这里我们也可以放入所有的Component的依赖(module和其他components)。
限定的component
可以通过SplashActivity
来探索Activities Components是怎么创建的。它重写了setupActivityComponent(AppComponent)
,在这里它创建它自己的Component(SplashActivityComponent
),然后注入所有被做@Inject
注解的依赖(这个例子中的SplashActivityPresenter
和AnalyticsManager
)。
在这里我们也会提供AppComponent实例(因为SplashActivityComponent
是依赖它的)以及SplashActivityModule
(它提供了Presenter和Activity实例)。
剩下的就靠你自己了。认真的说 - 尝试弄清楚一切是怎么合适地联系在一起的。下一章节我们尝试深入Dagger 2的那些紧密的元素(在底层它们是怎么工作的)。
代码:
以上描述的完整代码可见Githubrepository。