原文链接:http://frogermcs.github.io/dependency-injection-with-dagger-2-the-api/
本文是展示在Android应用中使用Dagger2进行依赖注入的系列文章之一。今天我想深入研究Dagger2的基础,并遍历这个依赖注入框架的所有API。
Dagger 2
前文我提到了DI框架带给我们的好处 - 它用少量代码绑定了所有依赖关系。Dagger2是一个DI框架的例子,它生成了大量的代码模板。但是,为什么它更优秀呢?现在,它是唯一一个模拟用户手写自动生成完全可追溯源码的DI框架。这意味着依赖关系图就是魔法门。Dagger2相较其他注入框架缺少动态性(未使用反射),但生成代码的简单性和效率与手写代码无异。
Dagger 2基础
Dagger2 API如下:
public @interface Component {
Class<?>[] modules() default {};
Class<?>[] dependencies() default {};
}
public @interface Subcomponent {
Class<?>[] modules() default {};
}
public @interface Module {
Class<?>[] includes() default {};
}
public @interface Provides {
}
public @interface MapKey {
boolean unwrapValue() default true;
}
public interface Lazy<T> {
T get();
}
还有其他几个JSR-330(Java中的依赖注入标准)定义的注解,也用在Dagger2中:
public @interface Inject {
}
public @interface Scope {
}
public @interface Qualifier {
}
让我们逐个击破。
@Inject注解
首先最重要的DI是@Inject
注解。作为JSR-330标准的一部分,标记那些依赖注入框架提供的依赖。Dagger2中,有3种不同的方式提供依赖:
构造器注入
构造器使用@Inject
:
public class LoginActivityPresenter {
private LoginActivity loginActivity;
private UserDataStore userDataStore;
private UserManager userManager;
@Inject
public LoginActivityPresenter(LoginActivity loginActivity,
UserDataStore userDataStore,
UserManager userManager) {
this.loginActivity = loginActivity;
this.userDataStore = userDataStore;
this.userManager = userManager;
}
}
所有参数都从依赖图中获取。@Inject
注解用在构造器还使得这个类成为依赖图的一部分。这意味它可以也随时注入,例如:
public class LoginActivity extends BaseActivity {
@Inject
LoginActivityPresenter presenter;
//...
}
本例中的限制是,我们只能对类中的一个构造器使用@Inject
注解.
域注入
另一个方法是对特殊域使用@Inject
注解:
public class SplashActivity extends AppCompatActivity {
@Inject
LoginActivityPresenter presenter;
@Inject
AnalyticsManager analyticsManager;
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
getAppComponent().inject(this);
}
}
本例中注入过程称为“手工”,方式如下:
public class SplashActivity extends AppCompatActivity {
//...
@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
getAppComponent().inject(this); //Requested depenencies are injected in this moment
}
}
该调用之前,我们的依赖值为null。
域注入的限制是域不能声明为private
。为什么?简而言之,生成的代码会明确的调用它们,像这样:
//This class is generated automatically by Dagger 2
public final class SplashActivity_MembersInjector implements MembersInjector<SplashActivity> {
//...
@Override
public void 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 class LoginActivityPresenter {
private LoginActivity loginActivity;
@Inject
public LoginActivityPresenter(LoginActivity loginActivity) {
this.loginActivity = loginActivity;
}
@Inject
public void enableWatches(Watches watches) {
watches.register(this); //Watches instance required fully constructed LoginActivityPresenter
}
}
所有方法参数由依赖图提供。但是为什么我们需要方法注入呢?当我们需要传类实例自身(this
引用)来注入到依赖时,我们需要提供函数注入。函数注入在构造器调用后立即调用,因此这意味着我们需要传递完全构造的this
。
@Module注解
@Module
是Dagger2 API的一部分。这个注解用于标记提供依赖的类 - 感谢这个注解,Dagger就知道在那里需要构造对象了。
@Module
public class GithubApiModule {
@Provides
@Singleton
OkHttpClient provideOkHttpClient() {
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setConnectTimeout(60 * 1000, TimeUnit.MILLISECONDS);
okHttpClient.setReadTimeout(60 * 1000, TimeUnit.MILLISECONDS);
return okHttpClient;
}
@Provides
@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();
}
}
@Provides注解
这个注解用在@Module
类。@Provides
标记Module中返回依赖的方法。
@Module
public class GithubApiModule {
//...
@Provides //This annotation means that method below provides dependency
@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();
}
}
@Component注解
这个注解用于构建接口,该接口把所有封装在一起。这里,我们定义需要依赖的模块(或组件)。这里定义了那些图依赖应当公开可见(可注入),我们的组件可以注入哪里。@Component
是连接@Module
和@Inject
的桥梁。
示例代码中@Component
使用了2个模块,可以向GithubClientApplication
注入依赖,并使其他3个依赖公共可见:
@Singleton
@Component(
modules = {
AppModule.class,
GithubApiModule.class
}
)
public interface AppComponent {
void inject(GithubClientApplication githubClientApplication);
Application getApplication();
AnalyticsManager getAnalyticsManager();
UserManager getUserManager();
}
而且@Component
可以依赖其他组件,可以定义生命周期(将来某期我将写作用域):
@ActivityScope
@Component(
modules = SplashActivityModule.class,
dependencies = AppComponent.class
)
public interface SplashActivityComponent {
SplashActivity inject(SplashActivity splashActivity);
SplashActivityPresenter presenter();
}
@Scope注解
@Scope
public @interface ActivityScope {
}
JSR-330标准的又一部分。在Dagger2中,@Scope
用来实现自定义作用域注解。总之,它使得依赖非常类似与单例。注解的依赖是单例的,但是也与组件的声明周期有关(不是整个应用)。但是如前所述 - 我们将在下一篇里深入研究作用域。现在,值得提醒的是所有自定义作用域行为是一样的(从代码角度) - 他们保持单一对象实例。但是它们也用在图形校验过程,该过程有助于尽快捕获依赖图的结构问题。
现在是一些不重要,不常使用的注解:
@MapKey
这个注解用于定义依赖集合(映射和集)。例子是自解释的:
定义
@MapKey(unwrapValue = true)
@interface TestKey {
String value();
}
提供依赖
@Provides(type = Type.MAP)
@TestKey("foo")
String provideFooKey() {
return "foo value";
}
@Provides(type = Type.MAP)
@TestKey("bar")
String provideBarKey() {
return "bar value";
}
使用
@Inject
Map<String, String> map;
map.toString() // => „{foo=foo value, bar=bar value}”
@MapKey
注解现在只支持两种类型的键值 - String和Enum。
@Qualifier
@Qualifier
注解帮助我们创建依赖的”tag”,依赖有同样的接口。想想一下,你需要提供两个RestAdapter
对象 - 一个用于Github API,零一二用于facebook API。
Qualifier
会帮助你识别它们:
命名依赖
@Provides
@Singleton
@GithubRestAdapter //Qualifier
RestAdapter provideRestAdapter() {
return new RestAdapter.Builder()
.setEndpoint("https://api.github.com")
.build();
}
@Provides
@Singleton
@FacebookRestAdapter //Qualifier
RestAdapter provideRestAdapter() {
return new RestAdapter.Builder()
.setEndpoint("https://api.facebook.com")
.build();
}
注入依赖
@Inject
@GithubRestAdapter
RestAdapter githubRestAdapter;
@Inject
@FacebookRestAdapter
RestAdapter facebookRestAdapter;
全部介绍完毕。我们已经了解了Dagger 2 API的所有重要组件。
App实例
现在,是时候上手练练了。我们将实现简单的Github客户端应用,该应用基于Dagger2构建。
想法
我们的Github客户端有3个页面和非经简单的用户用例。流程如下:
- 输入Github用户名
- 如果存在就显示所有的公开仓库列表
- 点击列表条目,显示仓库详情
应用样子如下:
从DI的角度,我们的架构如下:
总之 - 每个activity有自己的依赖图。每个图(_Component
类)有两个对象 - _Presenter
和_Activity
。而且每个对象从全局组件得到依赖 - AppComponent
,它包含其他组件Application
, UserManager
,RepositoriesManager
等。
说道AppComponent
,只要仔细观察这个接口。他包含两个模块:AppModule
和
GithubApiModule
。
GithubApiModule
提供如OkHttpClient
和RestAdapter
的依赖,这些依赖值用在这个模块中的其他依赖。在Dagger2中,我们可以控制哪些对象在组件外可见。在我们的例子中,我们不想暴露提到的对象。相反我们只暴露了UserManager
和RepositoriesManager
,因为只有这些对象用在我们的Activities中。所有这些通过返回值为非空类型且无参的公共方法定义。
文档中的例子:
预设方法
SomeType getSomeType();
Set<SomeType> getSomeTypes();
@PortNumber int getPortNumber();
并且我们也定义了我们想注入的依赖(通过方法注入)。例子里,AppComponent
未注入到任何地方,因为它只用作我们作用域组件的一个依赖。并且每个都定义了inject(_Activity activity)
方法。这里我们有简单的规则 - 使用方法定义注入,该方法只有一个参数(注入到我们的依赖中),名字无所谓,但必须返回void或者传单参数类型。
文档中的例子:
成员注入方法
SomeType getSomeType();
Provider<SomeType> getSomeTypeProvider();
Lazy<SomeType> getLazySomeType();
实现
我不会涉及过多的点。相反只需要克隆GithubClient代码,并在最新的Android Studio中引入即可。这里有一些要点:
Dagger2安装
仅需检查/app/build.gradle
文件。我们所需做的是增加Dagger2依赖并使用android-apt插件来连接Android Studio IDE和生成的代码。
AppComponent
从GithubClientApplication
类开始探索GithubClient工程。这里创建并保存了AppComponent
文件。这意味着所有单例对象将与Applicaiton对象(所有时期内)同生共死。
AppComponent
实现由Dagger 2生成的代码提供(使用构建模式创建对象:Dagger{ComponentName}.builder()
)。而且这有集合了所有组件的依赖(模块和其他组件)。
区域组件
为了检查Activities组件如何创建的,只需要从SplashActivity
开始查看。它重写了setupActivityComponent(AppComponent)
,那里它创建了自己的组件SplashActivityComponent
,并注入到所有@Inject
注解的依赖(本例是
SplashActivityPresenter
和AnalyticsManager
)。
这里我们提供了AppComponent实例(因为SplashActivityPresenter
依赖它),还有SplashActivityModule
(提供Presenter和Acitivyt实例)。
剩下的就靠你了。认真的——揣摩一切如何结合在一起。而在接下来的文章中我们会尽量密切关注Dagger2(它是如何工作的)。
##工程的全部源码在这里。
###作者
Azimo移动部门总监
如果你喜欢这个文章,你可以分享给你的伙伴或者Twitter上关注我。
写于2015,6月10日。