一、啥是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
的一種實現而已。