轉(zhuǎn)載請標(biāo)明原文地址:http://www.reibang.com/p/dc163215bc7e
本來打算繼續(xù)寫 MVP 模式的莲蜘,但是看了網(wǎng)上的幾篇 Dagger 介紹的文章后,還是決定先寫寫 Dagger帘营,網(wǎng)上有些文章寫的不是過于簡單就是太過復(fù)雜票渠,或是不夠詳實,讓剛接觸 Dagger 的人容易看的云里霧里芬迄。正好也是剛學(xué)習(xí) Dagger 沒多久问顷,記錄下來對自己也是一個查缺補漏。文中如有錯誤薯鼠,請各位大佬予以斧正!
本文示例代碼:https://github.com/junerver/DaggerDemo
Dagger2
注:本文中的 Dagger 都是指 Google 推出的 Dagger2
Dagger 是 Square 公司推出的一個 DI(依賴注入)框架械蹋,后來項目被 Google 接手出皇,大家習(xí)慣性稱之為 Dagger2。
依賴注入:可能有的朋友看到依賴注入這四個字就迷惑了哗戈,這是什么gui郊艘?那你聽聽控制反轉(zhuǎn)呢?是不是更難懂唯咬,更加拗口纱注。其實在我們的程序中存在著大量的依賴,這里的依賴不是指我們的項目依賴第三方庫胆胰,而是指我們的對象依賴于其他的實例狞贱。只要實例 A 中用到了 B 的實例,我們就稱之為 A 依賴于 B蜀涨。比如StringBuffer stringBuffer = new StringBuffer("hello world");
這里的 StringBuffer 類就依賴于 String 類瞎嬉。
在 J2EE 領(lǐng)域依賴注入使用很普及,對于大型項目而言存在著大量的實例厚柳,這些實例之間互相依賴氧枣,為了方便調(diào)用者使用,依賴注入順勢而生别垮,比如 Spring 框架中就包含了依賴注入的功能便监。
在 Android 中依賴注入起步較晚,其原因大概是因為早期的 Android 工程普遍不大碳想,而現(xiàn)在的 Android 工程動輒上百近千個頁面烧董,已經(jīng)可以視為大工程來看待的了毁靶,所以依賴注入框架也開始漸漸流行起來(同理像一些ORM框架也是這樣的);
釀酒大師教你釀酒
上面我們提到了為什么依賴注入開始流行起來解藻,我們來看看不使用依賴注入的一個示例代碼吧:
//制作白蘭地的流程
new Brandy(new Distiller(), new Wine(new Grape("解百納"), new FermentBarrel()));
看了這段代碼我估計你要罵街了老充,這是什么鬼!螟左?不是說要介紹依賴注入的優(yōu)勢么啡浊,這一大堆的 new 是什么東西?
別急別急胶背,沒看我的注釋是“制作白蘭地的流程”么巷嚣,我這是在制作一款用解百納釀造的白蘭地啊~,你看我們想要喝白蘭地需要先有酒液原漿吧钳吟,需要有蒸餾器吧廷粒,把原漿蒸餾了才能得到白蘭地嘛。而酒液原漿的獲得又需要用到葡萄和發(fā)酵桶嘛红且。
其實上面這段代碼我們就是模擬了一個相對復(fù)雜的實例化過程坝茎,可以看到其中的依賴關(guān)系如下
|--- 蒸餾器
白蘭地
| |--- 葡萄
|--- 原漿
|--- 發(fā)酵桶
那么在每次我們視圖實例化“白蘭地”對象時,都需要首先將其他的四個對象先行實例化完畢暇番,實例化一次還好嗤放,如果這個實例在上百個頁面中都需要使用到呢?但如果我們要是使用了依賴注入框架壁酬,將會使這一步驟變得十分容易次酌。
@Inject
Brandy mBrandy;
...
...
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
...
...
mInjection.inject(this);
}
是的,我們只需要在申明對象時舆乔,加上一行注解即可岳服。然后在需要使用的地方直接使用mBrandy
,是不是方便了很多希俩。
如何使用
添加依賴
看完上面的代碼對比吊宋,相比你已經(jīng)對 Dagger 產(chǎn)生了強烈的興趣了吧。如此優(yōu)雅颜武、高效的方式怎么會不吸引人呢贫母,下面我們來看看如何在項目中使用 Dagger 。
注意:我使用的環(huán)境是 AS 2.0盒刚,使用的 Dagger 版本是 2.4腺劣。如果你是使用 AS 2.2,請參照Dagger2官方的說明因块。
首先我們需要在項目中加入 Dagger 的依賴橘原,首先在項目的build.gradle文件中添加classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.0.0'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'// <----添加這一句
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
然后在 app 模塊下的build.gradle文件中添加apply plugin: 'com.neenbedankt.android-apt'
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:25.0.1'
//引入dagger2
compile 'com.google.dagger:dagger:2.4'
apt 'com.google.dagger:dagger-compiler:2.4'
//java注解
provided 'org.glassfish:javax.annotation:10.0-b28'
}
然后選擇 Sync Now即可;
了解幾個小概念
-
@Inject
@Inject 有兩個功能:
1、注解類的構(gòu)造方法趾断,其作用可以理解為通過注解將這個類的實例化方法告訴 Dagger拒名;
public class Distiller {
@Inject
public Distiller() {
}
@Override
public String toString() {
return "蒸餾器";
}
}
2芋酌、在需要使用該實例的地方注解同云,其作用是告訴調(diào)用者炸站,這個被注解的實例由 Dagger 來負(fù)責(zé)實例化旱易;
@Inject
Distiller mDistiller;
-
@Module
@Module 可以理解為一個生產(chǎn)實例的工廠,他掌握各個需要注入的類的實例化方法腿堤,當(dāng) Dagger 需要為某個類注入實例時阀坏,會到 @Module 注解的類中,查找這個類的實例化方法笆檀。當(dāng)然這一過程是需要通過使用 @Provides 注解的有返回值的方法忌堂,來告知 Dagger 的。
@Module
public class BrandyModule {
@Provides
public Grape provideGrape() {
return new Grape("解百納");
}
}
-
@Component
@Component 用來注解一個接口误债,在編譯的時候會生成 Dagger+文件名 的新Java文件浸船。Component可以理解為注射器妄迁,它是連接被注入的類與需要被注入的類之間的橋梁寝蹈。
@Component(modules = BrandyModule.class) //這里的modules參數(shù)可以是多個,用于告訴“注射器”都有哪些實例可以被注射到目標(biāo)類中
public interface BrandyComponent {
void inject(MainActivity mainActivity);
}
-
@Provides
在提供實例的方法上注解登淘,用于告訴 Dagger 這是一個用于注入的實例箫老。方法名可以隨便黔州,Dagger 是通過方法的返回值來將其添加到依賴列表的。
@Provides
public Grape provideGrape() {
return new Grape("解百納");
}
看個小例子
如果你按照上面的步驟寫下了代碼,運行代碼你會發(fā)現(xiàn)報了一個空指針錯誤涣达,這是因為我們還沒有為我們要注入的對象與被注入的類建立聯(lián)系。我們需要在使用被注入實例之前調(diào)用如下方法DaggerBrandyComponent.create().inject(this);
,注意這里的參數(shù) this ,這個參數(shù)的值由我們剛剛定義的接口 BrandyComponent 中聲明的方法的參數(shù)決定谨设。為了方便測試素跺,我新建了一個 TestDagger 類,并在接口中添加了一個方法void inject(TestDagger testDagger);
public class TestDagger {
@Inject
Distiller mDistiller;
public TestDagger() {
DaggerBrandyComponent.create().inject(this); //在代碼中我們并沒有對 Distiller 對象進行 new 操作來實例化
}
@Override
public String toString() {
return mDistiller.toString();
}
}
這樣做是為了可以方便的在測試用例中進行測試,而不用將程序運行在手機或者模擬器上。
可以看到,下面輸出了“蒸餾器”碴里,這說明我們對這一實例的注入成功了睡互。
不得不說的Module
看了上面的例子,你是不是覺得有點意思了图贸?但是要注意的是撒汉,上面的那種方法適用于無參的構(gòu)造方法(當(dāng)然也可以有參數(shù),但是對應(yīng)的參數(shù)的構(gòu)造方法上也要有 @Inject 注解)。Talk is cheap隘谣,show me the code秩仆!
為了驗證剛剛提到的那一點忘衍,我們來為蒸餾器的構(gòu)造方法添加一個新的參數(shù) Heater 加熱器铅搓!
public class Distiller {
private Heater mHeater;
@Inject
public Distiller(Heater heater) {
mHeater = heater;
}
@Override
public String toString() {
return "蒸餾器";
}
}
public class Heater {
public Heater() {
}
@Override
public String toString() {
return "加熱器";
}
}
注意上面的代碼,Heater 類的構(gòu)造方法沒有使用 @Inject 注解,我們運行一下看看效果。
程序拋出錯誤,錯誤的內(nèi)容是:在沒有使用 @Inject 注解構(gòu)造方法或者 @Provides 注解一個方法時無法提供 Heater 的實例。我們只需要在 Heater 類的構(gòu)造方法上也加上 @Inject 注解就可以了斑响。
通過上面的例子重绷,你應(yīng)該了解了我們可以為構(gòu)造方法參數(shù)的構(gòu)造方法添加 @Inject 注解來實現(xiàn)注入。(繞口令:八百標(biāo)兵奔北坡能颁,北坡炮兵并排跑敌土,炮兵怕把標(biāo)兵碰,標(biāo)兵怕碰炮兵炮)财剖。
但是如果我們的參數(shù)是第三方的類呢?比如參數(shù)是一個 String 呢策彤?我們不可能去 String 類的構(gòu)造方法中添加注解店诗。這時候就需要用到 Moudle 類了。
Module 的代碼上面我們已經(jīng)寫過了擦囊,我們來看一下
@Module
public class BrandyModule {
@Provides
public Grape provideGrape() {
return new Grape("解百納");
}
}
我們的 Grape 葡萄類的構(gòu)造方法有一個 String 參數(shù)涧郊,通過 @Provides 注解,我們可以告知 Dagger 當(dāng)需要用到 Grape 類的實例的時候幌陕,來 Module 類中獲取暇赤。再次運行代碼查看結(jié)果:
可以看到我們輸出了正確的結(jié)果:我們來梳理一下 Dagger 注入實例的過程:
- 步驟1:查找Module中是否存在創(chuàng)建該類的方法失暴。
- 步驟2:若存在創(chuàng)建類方法古戴,查看該方法是否存在參數(shù)
- 步驟2.1:若存在參數(shù)黍檩,則按從步驟1開始依次初始化每個參數(shù)
- 步驟2.2:若不存在參數(shù),則直接初始化該類實例润文,一次依賴注入到此結(jié)束
- 步驟3:若不存在創(chuàng)建類方法头谜,則查找Inject注解的構(gòu)造函數(shù)砖织,看構(gòu)造函數(shù)是否存在參數(shù)
- 步驟3.1:若存在參數(shù)甲脏,則從步驟1開始依次初始化每個參數(shù)
- 步驟3.2:若不存在參數(shù),則直接初始化該類實例,一次依賴注入到此結(jié)束
注:同時存在 @Inject 與 Module 時哲鸳,Module 的優(yōu)先級高于 @Inject 注解。
本文示例代碼:https://github.com/junerver/DaggerDemo
酒鬼總是希望可以多喝幾種酒
現(xiàn)在你應(yīng)該理解了 Dagger 是怎么一個工作流程了吧!也許你會問了,紀(jì)然 Dagger 是通過被注解方法的返回值來將它添加到依賴列表的,那么我們?nèi)绻卸鄠€ Grape 實例可用,應(yīng)該怎么辦呢(1、如何為 Dagger 創(chuàng)建多個相同類的的實例;2疾渣、在需要注入時如何區(qū)分多個實例;)?
首先我們再添加一個 @Provides 注解方法試試看:
可以看到梁沧,編譯器報錯,提示 bound multiple times(多次綁定)芝囤,難道說 Dagger 只能注入一種實例么??那他的局限性也太大了吧瘩扼?這時候 @Named 注解就需要粉墨登場啦~
@Named 注解用于給 @Provides 注解提供別名垃僚,在使用的時候也需要加上 @Named 注解集绰,Dagger 就知道我們需要的是具體哪個實例了。
@Provides
@Named("CabernetSauvignon")
public Grape provideOtherGrape() {
return new Grape("赤霞珠");
}
可以看到我們現(xiàn)在可以通過 @Named 注解來活動不同的葡萄了谆棺,那需要使用 Wine 類如果希望使用赤霞珠作為參數(shù)應(yīng)該怎么辦呢栽燕,如下所示:
@Provides
@Named("CabernetSauvignon")
public Wine provideOtherWine(@Named("CabernetSauvignon") Grape grape, FermentBarrel fermentBarrel) {
return new Wine(grape, fermentBarrel);
}
我們新增加一個提供赤霞珠原漿的方法,在其參數(shù)中使用了@Named("CabernetSauvignon")
來指定,這個參數(shù)是赤霞珠斧蜕。完整的代碼如下所示:
@Module
public class BrandyModule {
@Provides
public Grape provideGrape() {
return new Grape("解百納");
}
@Provides
@Named("CabernetSauvignon")
public Grape provideOtherGrape() {
return new Grape("赤霞珠");
}
@Provides
@Named("CabernetSauvignon")
public Wine provideOtherWine(@Named("CabernetSauvignon") Grape grape, FermentBarrel fermentBarrel) {
return new Wine(grape, fermentBarrel);
}
@Provides
@Named("CabernetSauvignon")
public Brandy provideOtherBrandy(@Named("CabernetSauvignon") Wine wine, Distiller distiller) {
return new Brandy(distiller, wine);
}
}
現(xiàn)在我們就可以品嘗到使用赤霞珠葡萄制作的白蘭地啦~
總結(jié)要點:
- @Inject 有兩種用途肛搬;
- 對于不能使用 @Inject 注解的類,將該類的實例化方法使用 @Provides 注解楼肪;
- 對于同一個類的不同實例化方法,使用 @Named 注解;
- @Named 注解還可以注解 Provides 方法的參數(shù);
我們不需要那么多的蒸餾器
我們將 Distiller 的 toString() 方法進行修改:
public class Distiller {
private Heater mHeater;
@Inject
public Distiller(Heater heater) {
mHeater = heater;
}
@Override
public String toString() {
return "有"+mHeater.toString()+"的蒸餾器"+super.toString();
}
}
再次運行程序:
發(fā)現(xiàn)問題了嗎嗽测?我們的白蘭地居然使用了不同的蒸餾器蜀变,這很不合理喂很,我們的釀酒作坊只需要一個蒸餾器就可以了,完全不需要對每瓶酒都使用一個新的蒸餾器湾盒。也就是說我們的 Distiller 類應(yīng)該是一個單例!
如果你看過其他文章你應(yīng)該會知道有一個注解 @Singleton,這個注解的字面就是單例,那么我們使用該注解來注釋我們的 Distiller 類以及我們的 BrandyComponent 接口墨坚。再次運行程序磷蛹,結(jié)果如下:
@Singleton
public class Distiller {
private Heater mHeater;
@Inject
public Distiller(Heater heater) {
mHeater = heater;
}
@Override
public String toString() {
return "有"+mHeater.toString()+"的蒸餾器"+super.toString();
}
}
@Component(modules = BrandyModule.class)
@Singleton
public interface BrandyComponent {
void inject(MainActivity mainActivity);
void inject(TestDagger testDagger);
}
你可能會驚嘆:我的天吶柿菩!真是魔法!Q恢隆!我們居然通過一個注解就實現(xiàn)了單例模式!误辑?還學(xué)什么7種單例模式的實現(xiàn)方式啊万俗,以后都用這個注解不就都搞定了嘛?
我們再新建一個測試類牧抵,同時在 @Component 中添加新的方法void inject(OtherTest otherTest);
public class OtherTest {
@Inject
Brandy mBrandy;
@Inject
@Named("CabernetSauvignon")
Brandy mCSBrandy;
public OtherTest() {
DaggerBrandyComponent.create().inject(this);
}
@Override
public String toString() {
return mBrandy.toString()+"\n"+mCSBrandy.toString();
}
}
在我們的測試用例中輸出System.out.println(new OtherTest().toString());
,結(jié)果如下所示:
可以看到我們又生成了一個新的蒸餾器,也就是說我們的 Distiller 類并不是一個真正的單例碳褒,但是在一個用例(一個被注入類)的范圍內(nèi),他確實是一個“單例”看疗。這也就是我所說的沙峻,@Scope 注釋的障眼法(@Singleton 的實質(zhì)就是一個 @Scope)。
@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
@Scope 字面意思是范圍两芳,實際使用的效果我們可以看出在相同范圍內(nèi)专酗,只會存在一個該實例。那么這個范圍到底是什么盗扇?我的理解是:調(diào)用注入者的生命周期祷肯,就是這個標(biāo)注的范圍。比如 TestDagger 類調(diào)用了 DaggerBrandyComponent.create().inject(this);
進行了注入疗隶,在這個類的生命周期里佑笋,會復(fù)用 Distiller 類的實例。
可以很容易的證明我上面的這段文字斑鼻,我們自行實現(xiàn)一個 Scope 注解:
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface Lalala {}
運行的結(jié)果與上面是完全相同的蒋纬!充分證明了 @Scope 注解的本質(zhì)就是在同生命周期中復(fù)用有注解的實例。
注:提供實例的方法坚弱、或者含有@Inject的類蜀备,其 Scope 名稱必須與對應(yīng)的 Component 完全一致!
只要一個蒸餾器
上面說了荒叶,所謂的 Singleton 注解只是一個官方已經(jīng)定義好的 @Scope碾阁,那我們怎么才能真正的實現(xiàn)一個單例的蒸餾器呢?
首先我們在介紹一個 @Component 注解的參數(shù) dependencies (依賴)些楣,通過依賴我們可以將注射器進行“繼承”脂凶,Show me the code!
新建一個接口愁茁,這個接口中有一個方法蚕钦,返回值是 Distiller,注意其中的 Scope 注解鹅很,使用的是與 Distiller 類相同的注解嘶居。那么這個 Component 可以為我們提供 Distiller 類的注入。注意其中方法名是可以隨便寫的促煮,這跟 @Provides 注解是一樣的邮屁,Dagger 只關(guān)心返回值整袁。
@Component
@Singleton
public interface BaseComponent {
Distiller anyName();
}
修改 BrandyComponent 類如下:
@Component(modules = BrandyModule.class,dependencies = BaseComponent.class)
@Lalala
public interface BrandyComponent {
void inject(MainActivity mainActivity);
void inject(TestDagger testDagger);
void inject(OtherTest otherTest);
}
注意這里我使用的是剛剛自行創(chuàng)建的 Scope 注解,因為 Component 的 Scope 不能相同樱报。重新編譯代碼后會發(fā)現(xiàn)報錯了葬项,是因為我們原來使用的注入是DaggerBrandyComponent.create().inject(this);
,當(dāng)我們?yōu)?BrandyComponent 添加依賴后迹蛤,就不能再使用 create 方法來生成 Component 的實例了民珍,只能使用 builder 方法來構(gòu)建,而且我們還必須要為 builder 添加 baseComponent(BaseComponent baseComponent) 這一方法盗飒;
前面我們已經(jīng)說到嚷量,@Scope 注解的本質(zhì)是在同生命周期內(nèi)復(fù)用實例。我們在一個單例中實現(xiàn) BaseComponent (單例模式的生命周期就是軟件的生命周期)逆趣,那么這個注射器可以注入的實例就將都是單例模式蝶溶。
為了驗證我們的說法:我們創(chuàng)建一個單例:
public class Singleton {
private BaseComponent mBaseComponent;
private Singleton() {
mBaseComponent = DaggerBaseComponent.create();
}
private static class SingletonInstance {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return SingletonInstance.INSTANCE;
}
public BaseComponent getBaseComponent() {
return mBaseComponent;
}
}
將原來的注入方法DaggerBrandyComponent.create().inject(this);
修改為
DaggerBrandyComponent
.builder()
.baseComponent(Singleton.getInstance().getBaseComponent())
.build()
.inject(this);
再次運行代碼:
這次我們就真正實現(xiàn)了被注入對象單例了!
在 Android 中我們有一個現(xiàn)成的單例模式可用宣渗,那就是我們的 Application 類抖所,我們只要寫下如下代碼就可以實現(xiàn)上述效果:
public class MyApp extends Application {
private BaseComponent mBaseComponent;
@Override
public void onCreate() {
super.onCreate();
mBaseComponent = DaggerBaseComponent.create();
}
public BaseComponent getBaseComponent() {
return mBaseComponent;
}
}
如果 BaseComponent 需要使用 Module 的話,就將 BaseComponent 實例獲取方式修改為: mBaseComponent = DaggerBaseComponent.builder().baseModule(new BaseModule()).build();
總結(jié)要點:
- Component 與 Module 的 Scope 必須相同痕囱;
- Component 與 被依賴的 Component 的 Scope 必須不同田轧;
- 如果 Component 有依賴,則只能使用 builder 方式來構(gòu)建 Component 對象鞍恢,同時必須傳入被依賴的 Component傻粘;
- 被依賴的 Component 提供的能被注入的實例,需要在接口中用方法聲明帮掉。
好啦弦悉,本篇文章到此也就告一段落了,對于 Dagger 的使用蟆炊,相比你也已經(jīng)有了一定的了解了稽莉,本文示例代碼在DaggerDemo,大家可以參考著代碼閱讀本文盅称,會對理解有更好的幫助肩祥!