一、啥是Dagger2
前面的概念可能开始看不懂,给点耐心,看到例子就懂了。 本篇文章需要注解方面的知识,不了解的可以先看:http://blog.csdn.net/niubitianping/article/details/60145128
Dagger2的内容有点多,一点得有耐心。
1.1 简介
Dagger2是一个Android/Java平台上快速依赖注入框架,由谷歌开发,最早的版本Dagger1 由Square公司开发。依赖注入框架主要用于模块间解耦,提高代码的健壮性和可维护性。
几大优点:
- 全局对象实例的简单访问方式,@Inject
- 复杂的依赖关系只需要简单的配置
- 让单元测试和集成测试更加方便
- 轻松指定作用域
github地址:
https://github.com/google/dagger
说明文档:
https://google.github.io/dagger/
1.1 主要元素
关系图:
主要元素有以下三个:
Container: 相当于Android的Activity,在activity里面获取其他类的实例
Component: 一个接口,告诉activty你要获取实例的类在哪里找
Module: activty要的东西就在这里初始化。
1.2 主要注解
看不懂下面的注解可以先看例子使用了,然后回来看就懂了。还有其他注解,在后面会讲到。
1. @Inject
通常在需要依赖的地方使用这个注解。你用它告诉Dagger这个类或字段需要依赖注入,,Dagger就会构建一个这个类的实例并满足他们的依赖。
2. @Module
用来修饰modules类。 所以我们定义一个类,用@Module注解,这样Dagger在构造类的实例时候,就知道从哪里去找到需要的依赖。modules的一个重要特征是它们设计为分区并组合在一起(比如说,我们的app中可以有很多在一起的modules)
3. @Provide
我们在modules中定义的方法就是是用这个注解来修饰,以此来告诉Dagger我们想要构造对象并提供这些依赖。
4. @Component
Components从根本上来说就是一个注入器,也可以说是@Inject
和@Module
的桥梁,他的主要作用就是链接这两个部分。Components可以提供所有定义了的类型的实例,比如: 我们必须用@Component注解一个接口然后列出所有的
二、Dagger2基本使用
2.1 导包
在项目的build.gradle添加:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.0'
//下面添加apt
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
....
在需要的Module的build.gradle添加两个部分:
apply plugin: 'com.android.application'
//添加的第一部分
apply plugin: 'com.neenbedankt.android-apt'
android {
......
}
dependencies {
......
//添加的第二部分,版本太高会导致编译失败,这里用2.8就可以了
compile 'com.google.dagger:dagger:2.8' //dagger的api
apt "com.google.dagger:dagger-compiler:2.8" //指定注解处理器
compile 'org.glassfish:javax.annotation:10.0-b28' //Adnroid缺失的部分javax注解
}
2.2 创建你要实例化的类
这里假如我想在Activity里面实例化一个LoginCtrl的类。于是创建一个LoginCtrl类
public class LoginCtrl {
public void login(String name,String pass){
Log.e("tag@@", "name:"+name+" pass:"+pass);
}
}
2.3 创建module类
在这个类里面 真正的实例化LoginCtrl类。创建一个LoginModule类,类使用@Module
修饰,然后里面添加方法provideLoginCtrl
(方法名随便),返回类型为LoginCtrl
,使用@Provides
修饰方法名。如下:
@Module
public class LoginModule {
@Provides LoginCtrl provideLoginCtrl(){
return new LoginCtrl();
}
}
2.4 创建Component接口
创建一个LoginComponent接口,用来告诉Activity你要实例化的东西在这里。 @Component
的参数提供Module进行联系, 接口里面的方法和activty进行联系。 这样形成了桥梁
PS:注意接口里面的方法必须要有参数,不然会编译错误
@Component(modules = LoginModule.class)
public interface LoginComponent {
//要添加方法,方法必须添加参数,参数类型必须和调用时候一致
void inject(TestActivity activity);
}
2.5 构建
搞完上面的步骤之后,点击菜单栏的Build -> Build Project 或者 Build Module。稍等一会儿。 等构建完成之后,就会在module -> 的build -> generated -> source -> apt -> debug -> 包名/路径 -> 看到生成对应的文件
2.6 在Activity注入
接下来就可以使用了,在activity中使用@Inject
修饰你要实例化的类,然后使用类Dagger+Compontent接口的类名
初始化Dagger,就成功注入了,我这里是新建的一个TestAtivity。
public class TestActivity extends Activity {
@Inject
LoginCtrl loginCtrl; //注入的方式实例化LoginCtrl
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//初始化注入,inject是LoginComponent接口里面自定义的方法;
DaggerLoginComponent.create().inject(this);
// 然后就可以调用loginCtrl里面的方法了
loginCtrl.login("tianping","pass");
}
}
就可以看到输出
com.tpnet.dagger2test E/tag@@: name:tianping pass:pass
PS注意:Component接口方法里面的参数,在Activity传递的时候必须类型一致,MainActivity就是MainActivity.this。如果参数类型是Context,你传递了MainActivity.this过去就会导致注入失败,实例化对象为空。
三、Module参数传递
把上面的栗子修改一下,添加两个类,在LoginCrtl
里面进行控制这两个类。
LoginStore.java类,看作为本地保存登录信息的类。
public class LoginStore {
private Context mContext;
public LoginStore(Context mContext) {
this.mContext = mContext;
}
public void login(String name,String pass){
Log.e("@@", "LoginStore进行保存: name="+name+",pass="+pass);
SharedPreferences.Editor editor = mContext.getSharedPreferences("login",Context.MODE_PRIVATE).edit();
editor.putString("name",name);
editor.putString("pass",pass);
editor.apply();
}
}
LoginService.java类,看作为链接网络登录的类。
public class LoginService {
public void login(String name,String pass){
//网络请求登录....
Log.e("@@", "LoginService登录: name="+name+",pass="+pass);
}
}
LoginCtrl修改为:
public class LoginCtrl {
private LoginStore mLoginStore;
private LoginService mLoginService;
public LoginCtrl(LoginService service, LoginStore store) {
this.mLoginStore = store;
this.mLoginService = service;
}
public void login(String name,String pass){
mLoginService.login(name,pass);
mLoginStore.login(name,pass);
}
}
LoginModule修改为:
@Module
public class LoginModule {
private Context mContext; //供给LoginStore使用
public LoginModule(Context mContext) {
this.mContext = mContext;
}
@Provides LoginCtrl provideLoginCtrl(LoginService service, LoginStore store){
return new LoginCtrl(service,store);
}
}
3.1 Module构造方法传参
看了上面修改完之后的代码,LoginModule类里面需要在构造方法里面传参,怎么传呢?
在activity初始化的时候使用builder:
public class TestActivity extends Activity {
@Inject
LoginCtrl loginCtrl;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//DaggerLoginComponent.create().inject(this);
//当Module需要构造方法传参的时候,使用builder的方式初始化Dagger。
DaggerLoginComponent.builder()
.loginModule(new LoginModule(this)) //loginModule这个方法是构建之后才有的
.build()
.inject(this);
loginCtrl.login("天平","密码");
}
}
总结就是:
当Module需要构造方法传参的时候,使用builder的方式初始化Dagger
3.2 module里面的方法参数
上面的修改完的代码运行肯定会报错的,报错信息如下:
原因就是在LoginModule里面,provideLoginCtrl方法有两个参数:LoginService和LoginStore,这俩参数并没有注解实例化,所以这里就报错了。 解决方法是下面两个。
- 通过构造方法@Inject
- 在Module类里面@Provide
3.2.1 通过构造方法Inject
module里面的方法需要参数的解决方法1: 通过构造方法@Inject
又分两种情况,带参数的构造方法和不带参数的。
- 不带参数的
如果Moudle里面的方法的参数这个类的构造方法不需要参数的,直接在构造方法添加@Inject
即可。
例如Loginservice,在LoginService里添加一个Inject构造方法:
public class LoginService {
//构造方法没有参数的,直接在构造方法用@Injext修饰即可
@Inject
public LoginService() {
}
public void login(String name, String pass){
//网络请求登录....
Log.e("@@", "LoginService登录: name="+name+",pass="+pass);
}
}
- 带参数的
如果Moudle里面的方法的参数这个类的构造方法需要带参数的。例如LoginStore,构造方法需要提供Context参数。Inject之后,还需要在Module类里面@Provide
一个String类型的方法,作为LoginStore构造方法的参数
LoginStore.java修改为:
public class LoginStore {
private Context mContext;
//这里@Inject构造方法,然后在Module类里面还需要Provide一个String的方法
@Inject
public LoginStore(Context mContext) {
this.mContext = mContext;
}
public void login(String name,String pass){
Log.e("@@", "LoginStore进行保存: name="+name+",pass="+pass);
SharedPreferences.Editor editor = mContext.getSharedPreferences("login",Context.MODE_PRIVATE).edit();
editor.putString("name",name);
editor.putString("pass",pass);
editor.apply();
}
}
LoginModule.java修改为i:
@Module
public class LoginModule {
private Context mContext; //供给LoginStore使用
public LoginModule(Context mContext) {
this.mContext = mContext;
}
//为LoginStore提供构造参数
@Provides Context provideStoreContext(){
return mContext;
}
@Provides LoginCtrl provideLoginCtrl(LoginService service, LoginStore store){
return new LoginCtrl(service,store);
}
}
好了,程序正常,这时候运行程序就会看到输出:
03-04 19:14:00.124 31170-31170/? E/@@: LoginService登录: name=天平,pass=密码
03-04 19:14:00.124 31170-31170/? E/@@: LoginStore进行保存: name=天平,pass=密码
3.2.2 在Module类里面@Provide
module里面的方法需要参数的解决方法1: 在Module类里面@Provide
一个参数类型。
- 我们把代码改回
3.2.1
之前那样 - 修改LoginModule增加两个方法
@Module
public class LoginModule {
private Context mContext; //供给LoginStore使用
public LoginModule(Context mContext) {
this.mContext = mContext;
}
/**
* 为provideLoginCtrl方法的service参数提供实例化
* @return
*/
@Provides LoginService provideLoginService(){
return new LoginService();
}
/**
* 为provideLoginCtrl方法的store参数提供实例化
* @return
*/
@Provides LoginStore provideLoginStore(){
return new LoginStore(mContext);
}
@Provides LoginCtrl provideLoginCtrl(LoginService service, LoginStore store){
return new LoginCtrl(service,store);
}
}
程序正常运行,看到输出内容为:
03-04 19:20:40.795 31739-31739/com.tpnet.dagger2test E/@@: LoginService登录: name=天平,pass=密码
03-04 19:20:40.795 31739-31739/com.tpnet.dagger2test E/@@: LoginStore进行保存: name=天平,pass=密码
简单概括: 有需有求,activity的Inject需要什么对象,Module类就提供什么对象。
四、Dagger2模块化
在1.1的关系图上面有说到多个Module , 说明了一个Component是可以依赖多个Module的,方法有三种:
- 多个@Module修饰类
- include @Moudle修饰类
- dependencies依赖Component
来逐个理解。
4.1 多个@Module修饰类
Component的值为@Module修饰类, 在@Component的接口里面需要添加Module类,如果需要依赖多个module类,用数组就行了。
再新建一个getInfoModule.java类:
@Module
public class getInfoModule {
}
在Logincomponent里面添加module数组即可
@Component(modules = {LoginModule.class,getInfoModule.class})
public interface LoginComponent {
void inject(TestActivity activity);
}
这是第一种模块化方法
4.2 include @Moudle修饰类
@Moudle修饰的类include @Moudle修饰类, 这里是在LoginModule类里面的@Module修饰符添加includes module
把4.1在LoginModule.java添加的代码删掉, 然后修改LoginModule的代码:
//这里includes了需要的Module
@Module(includes = getInfoModule.class)
public class LoginModule {
private Context mContext; //供给LoginStore使用
public LoginModule(Context mContext) {
this.mContext = mContext;
}
....(下面的代码就不拷贝了)
}
4.3 dependencies依赖Component
Component 依赖dependencies Component, 新建一个GetInfoComponent.java 接口,在里面依赖需要的Module:
@Component(modules = getInfoModule.class)
public interface GetInfoComponent {
}
然后在LoginComponent.java里面使用dependencies依赖GetInfoComponent.class
@Component(modules = LoginModule.class,dependencies = GetInfoComponent.class)
public interface LoginComponent {
void inject(TestActivity activity);
}
五、创建区分不同实例
问题1. Activity里面@Inject的类是根据什么初始化的呢?
其实是根据Module类里面的方法的返回类型进行判断。
问题2. 那么问题就来了,加入我在activity需要注入两个相同类型的类呢? 怎么区分呢?
有两种方法:
- 在Module里面的方法和Activity Inject的类上面添加
@Named(value)
修饰,value就是用以区分 - 自定义注解。
5.1 方法1:@Named(value)区分
在3.2.2的代码上进行修改,在TestActivity再Inject一个LoginCtrl:
public class TestActivity extends Activity {
@Named("one") //用以区分LoginCtrl实例
@Inject
LoginCtrl loginCtrlOne;
@Named("two") //用以区分LoginCtrl实例
@Inject
LoginCtrl loginCtrlTwo;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//DaggerLoginComponent.create().inject(this);
//当Module需要构造方法传参的时候,使用builder的方式初始化Dagger。
DaggerLoginComponent.builder()
.loginModule(new LoginModule(this)) //loginModule这个方法是构建之后才有的
.build()
.inject(this);
loginCtrlOne.login("天平one","密码one");
loginCtrlTwo.login("天平two","密码two");
}
}
在LoginModule.java里面编辑添加一个返回LoginCtrl的方法
@Module
public class LoginModule {
private Context mContext; //供给LoginStore使用
public LoginModule(Context mContext) {
this.mContext = mContext;
}
/**
* 为provideLoginCtrl方法的service参数提供实例化
* @return
*/
@Provides
LoginService provideLoginService(){
return new LoginService();
}
/**
* 为provideLoginCtrl方法的store参数提供实例化
* @return
*/
@Provides
LoginStore provideLoginStore(){
return new LoginStore(mContext);
}
@Named("one") //用以区分LoginCtrl实例
@Provides
LoginCtrl provideLoginCtrlOne(LoginService service, LoginStore store){
Log.e("@@", "provideLoginCtrlOne被调用: ");
return new LoginCtrl(service,store);
}
@Named("two") //用以区分LoginCtrl实例
@Provides
LoginCtrl provideLoginCtrlTwo(LoginService service, LoginStore store){
Log.e("@@", "provideLoginCtrlTwo被调用: ");
return new LoginCtrl(service,store);
}
}
可以看到输出内容为一下,程序正常:
03-04 22:30:24.051 13833-13833/? E/@@: provideLoginCtrlOne被调用:
03-04 22:30:24.051 13833-13833/? E/@@: provideLoginCtrlTwo被调用:
03-04 22:30:24.051 13833-13833/? E/@@: LoginService登录: name=天平one,pass=密码one
03-04 22:30:24.051 13833-13833/? E/@@: LoginStore进行保存: name=天平one,pass=密码one
03-04 22:30:24.061 13833-13833/? E/@@: LoginService登录: name=天平two,pass=密码two
03-04 22:30:24.061 13833-13833/? E/@@: LoginStore进行保存: name=天平two,pass=密码two
5.2 方法2:自定义注解
我们按住Ctrl,然后鼠标点击@Named
,可以看到他的注解源码为:
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
/** The name. */
String value() default "";
}
主要就是@Qualifier
这个注解,我们也可以自己定义注解来区分。
5.2.1 使用value
创建一个注解,例如TPTest.java:
@Qualifier
@Retention(RUNTIME)
public @interface TPTest {
String value() default "";
}
然后把5.1的例子的@Named改为@TPTest,你会发现也是可以的。。
5.2.2 不使用value
5.2.1是定义了一个注解,利用里面的value进行区分,也可以用两个注解进行区分。
新建一个One注解:
@Qualifier
@Retention(RUNTIME)
public @interface One {
//这里没有value
}
再新建一个Two注解
@Qualifier
@Retention(RUNTIME)
public @interface Two {
//这里没有value
}
然后把之前的@TPTest("one")
改为@One
, @TPTest("two")
改为@Two
, 运行你会发现还是一样的。
5.3 @Qualifier注解
在刚刚自定义注解的时候可以看到Qualifier
这个关键词,这个关键词的作用就是: 用来区分不同的对象实例,@Named
是@Qualifier
的一种实现而已。