Dagger2 入門,以初學者角度.

Dagger2 入門


本文屬於轉載,原文地址:http://blog.izouxiang.cn/2016/10/22/Dagger2%20%E5%85%A5%E9%97%A8/

本文比較詳細的介紹了Dagger2的一些基礎註解的用法,跟着文章的思路去好好去練習,對你的入門是有很大幫助的。閱讀完本篇文章,我收穫頗多,弄懂了Dagger2的注入思路。所以在此轉載給更多的人學習。

跟着文章,我敲了一遍,已上傳到github。小夥伴們可以參考參考。

項目地址:https://github.com/weioule/Dagger2

依賴注入

Dagger2是Android中比較熱門的依賴注入框架,什麼是依賴注入呢?維基百科上是這樣描述的:

控制反轉(Inversion of Control,縮寫爲IoC),是面向對象編程中的一種設計原則,可以用來減低計算機代碼之間的耦合度。其中最常見的方式叫做依賴注入(Dependency Injection,簡稱DI),還有一種方式叫“依賴查找”(Dependency Lookup)。通過控制反轉,對象在被創建的時候,由一個調控系統內所有對象的外界實體,將其所依賴的對象的引用傳遞給它。也可以說,依賴被注入到對象中.

通俗的來講呢,就是一個類中需要依賴其他對象時,不需要你親自爲那些需要依賴的對象賦值,爲那些對象賦值的操作交給了IOC框架.

Dagger2介紹

一般的IOC框架都是通過反射來實現的,但Dagger2作爲Android端的IOC框架,爲了不影響性能,它是通過apt動態生成代碼來實現的.
Dagger2主要分爲三個模塊:

  1. 依賴提供方Module,負責提供依賴中所需要的對象,實際編碼中類似於工廠類
  2. 依賴需求方實例,它聲明依賴對象,它在實際編碼中對應業務類,例如Activity,當你在Activity中需要某個對象時,你只要在其中聲明就行,聲明的方法在下面會講到.
  3. 依賴注入組件Component,負責將對象注入到依賴需求方,它在實際編碼中是一個接口,編譯時Dagger2會自動爲它生成一個實現類.

Dagger2的主要工作流程分爲以下幾步:

  1. 將依賴需求方實例傳入給Component實現類
  2. Component實現類根據依賴需求方實例中依賴聲明,來確定該實例需要依賴哪些對象
  3. 確定依賴對象後,Component會在與自己關聯的Module類中查找有沒有提供這些依賴對象的方法,有的話就將Module類中提供的對象設置到依賴需求方實例中

通俗上來講就好比你現在需要一件衣服,自己做太麻煩了,你就去商店買,你跟商店老闆說明你想要購買的類型後,商店老闆就會在自己的衣服供應商中查找有沒有你所說的類型,有就將它賣給你.其中你就對應上面所說的依賴需求方實例,你只要說明你需要什麼,商店老闆則對應Component實現類,負責滿足別人的需求,而衣服供應商則對應Module類,他負責生產衣服.也許這裏有點繞,但經過下面的Demo,也許能夠幫助你理解.

書寫Demo


引入Dagger2

在項目下的build.gradle文件中添加apt插件:

buildscript {
    ...
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.2'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
        //添加apt插件
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

...

在app目錄的build.gradle文件中添加:

//應用apt插件
apply plugin: 'com.neenbedankt.android-apt'

...

dependencies {
    ...
    //引入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'
}

編寫布料類Cloth

寫一個Cloth類用作依賴對象,它包含一個color屬性

public class Cloth {
    private String color;

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    @Override
    public String toString() {
        return color + "布料";
    }
}

書寫Module類

現在的需求是MainActivity中需要使用到Cloth對象,所以我們要爲MainActivity書寫一個Module類用來提供Cloth對象,相當於創建了一個提供商

@Module
public class MainModule {

    @Provides
    public Cloth getCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
}

嗯?怎麼多了兩個註解?這兩個註解有什麼用呢?
註解是Dagger2中的關鍵,編寫Module類時要在該類上聲明@Module以表明該類是Module類,這樣Dagger2才能識別,那@Provides又是幹嘛的呢?它的作用是聲明Module類中哪些方法是用來提供依賴對象的,當Component類需要依賴對象時,他就會根據返回值的類型來在有@Provides註解的方法中選擇調用哪個方法.在一個方法上聲明@Provides註解,就相當於創建了一條生產線,這條生產線的產物就是方法的返回值類型.有了這條生產線,供應商就能提供這種類型的商品了,當商店老闆發現有人需要這種類型的商品時,供應商就可以提供給他了

書寫Component接口

@Component(modules=MainModule.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

和Module類一樣,Component類也是需要註解聲明的,那個註解就是@Component,但是@Component註解的作用可不是單單用來聲明Component類,他還有更強大的功能,@Component註解有modules和dependencies兩個屬性,這兩個屬性的類型都是Class數組,modules的作用就是聲明該Component含有哪幾個Module,當Component需要某個依賴對象時,就會通過這些Module類中對應的方法獲取依賴對象,MainComponent中只包含MainModule,所以令modules=MainModule.class,相當於供應商和商店老闆確定合作關係的合同.而dependencies屬性則是聲明Component類的依賴關係,這個下面再詳講.
接口中那個方法又是幹嘛用的呢?
我們現在只是聲明瞭Component類,但我們要怎麼將Component類和依賴需求方對象聯合起來呢?答案就是通過這個inject方法,這個方法可以將依賴需求方對象送到Component類中,Component類就會根據依賴需求方對象中聲明的依賴關係來注入依賴需求方對象中所需要的對象,本Demo中MainActivity中需要Cloth對象,所以我們通過inject方法將MainActivity實例傳入到MainComponent中,MainComponent就會從MainModule中的getCloth方法獲取Cloth實例,並將該實例賦值給MainActivity中的cloth字段.相當於你去商店的道路,沒有這條路,你就無法去商店和老闆說明你所需要的東西.但是這裏需要注意的是,inject方法的參數不能用父類來接收,例如本Demo中,如果inject的參數是Activity,那麼Dagger2就會報錯.

在MainActivity中聲明

public class MainActivity extends AppCompatActivity {
    private TextView tv;
    @Inject
    Cloth cloth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);

        MainComponent build = DaggerMainComponent.builder().mainModule(new MainModule()).build();
        build.inject(this);
        tv.setText("我現在有" + cloth);
    }
}

上面代碼中有兩處關鍵:

  1. 聲明依賴對象Cloth,就是在cloth字段上添加@Inject註解,Dagger2中聲明依賴對象都是通過@Inject註解,但是@Inject註解的字段不能是private和protected的.
  2. 通過Dagger2自動生成的類來創建Component的實現類,創建時需要傳入該Component實現類所需要的Module類實例,傳入方法就是調用Module類類名首字母小寫對應的方法.這裏我們通過Dagger2自動生成的DaggerMainComponent類創建了MainComponent的實例,相當於我們創建了一個實實在在的商店,不再是理論上的商店,但是創建商店一定也要創建真實的供應商嘛,所以創建Component實現類時一定要傳入Module的實例.(注意編寫完Component接口後Dagger2並不會自動創建對應的類,需要我們點擊Android Studio中bulid菜單下的Rebulid Poject選項,或者直接書寫代碼,編譯時Dagger2就會幫你自動生成).
    再將MainActivity通過inject方法發送到MainComponent中,調用完inject方法後,你就會發現,MainActivity中的cloth字段已經被賦值,而且該cloth對應的就是我們在MainModule類getCloth方法中創建的Cloth對象.

結果

dagger2demo_1.jpeg

另一種方法

前面的Demo可能給人最大的感受就是麻煩吧?就是爲cloth賦個值,又要寫什麼Module類,又是要寫什麼Component接口.其實Dagger2還可以用註解來提供依賴對象.讓我們來瞧瞧怎麼使用.

創建依賴類Shoe

我們又創建一個依賴類Shoe

public class Shoe {
    @Inject
    public Shoe() {
    }

    @Override
    public String toString() {
        return "鞋子";
    }
}

但是這次我們創建的方式和Cloth不一樣了,我們在構造函數上聲明瞭@Inject註解,這個註解有什麼用呢?作用可大了,當Component在所擁有的Module類中找不到依賴需求方需要類型的提供方法時,Dagger2就會檢查該需要類型的有沒有用@Inject聲明的構造方法,有則用該構造方法創建一個.
相當於你去商店購買東西,你需要的東西商店的供應商不生產,商店老闆就只好幫你去網上看看有沒有你需要的東西,有則幫你網購一個.(假設你不會網購,哈哈^ ^).

在MainActivity中聲明Shoe依賴

我們修改之前的MainActivity,添加一點東西

public class MainActivity extends AppCompatActivity {
    ...
    @Inject
    Shoe shoe;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText("我現在有" + cloth + "和" + shoe);
    }
}

結果

dagger2demo_2.jpeg

注意

有些讀者可能會這樣想:爲什麼不都用這種方法來聲明呢?爲什麼要用Module類?
答案是這樣的,項目中我們會用到別人的jar包,我們無法修改別人的源碼,就更別說在人家的類上添加註解了,所以我們只能通過Module類來提供.

複雜一點的情況

我們創建的這些依賴類都不用依賴於其它依賴類,但是如果需要依賴於其它依賴類又要怎麼弄呢?

創建依賴類Clothes

我們又來創建一個衣服類Clothes,製作衣服時需要布料,所以我們在創建Clothes的實例時需要用到Cloth實例

public class Clothes {
    private Cloth cloth;

    public Clothes(Cloth cloth) {
        this.cloth = cloth;
    }

    public Cloth getCloth() {
        return cloth;
    }

    @Override
    public String toString() {
        return cloth.getColor() + "衣服";
    }

}

在Module類中增加提供方法

現在我們的MainActivity中需要依賴於Clothes對象,所以我們在MianModule中添加提供Clothes對象的方法,但是Clothes需要依賴於Cloth對象,這要怎麼辦呢?可能最先想到的辦法就是這樣:

    @Provides
    public Clothes getClothes(){
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return new Clothes(cloth);
    }

直接在方法中創建一個Cloth不就得了,但是你有沒有發現,創建Cloth的代碼已經在getCloth方法中有了,我們能不能用getCloth方法中創建的Cloth實例來創建Clothes實例呢?
Dagger2提供了這樣的功能,我們只要在getClothes方法中添加Cloth參數,Dagger2就會像幫依賴需求方找依賴對象一樣幫你找到該方法依賴的Cloth實例,所以我們代碼可以這樣改:

    @Provides
    public Clothes getClothes(Cloth cloth){
        return new Clothes(cloth);
    }

在MainActivity中聲明Clothes依賴

我們修改之前的MainActivity,添加一點東西

public class MainActivity extends AppCompatActivity {
    ...
    @Inject
    Clothes clothes;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText("我現在有" + cloth + "和" + shoe + "和" + clothes);
    }
}

結果

dagger2demo_3.jpeg

依賴總結

同理,在帶有@Inject註解的構造函數要是依賴於其它對象,Dagger2也會幫你自動注入.筆者就不測試了,希望讀者親測一下.
這裏我們引用依賴注入神器:Dagger2詳解系列中的一段話:

我們有兩種方式可以提供依賴,一個是註解了@Inject的構造方法,一個是在Module裏提供的依賴,那麼Dagger2是怎麼選擇依賴提供的呢,規則是這樣的:

  • 步驟1:查找Module中是否存在創建該類的方法。
  • 步驟2:若存在創建類方法,查看該方法是否存在參數
  • 步驟2.1:若存在參數,則按從步驟1開始依次初始化每個參數
  • 步驟2.2:若不存在參數,則直接初始化該類實例,一次依賴注入到此結束
  • 步驟3:若不存在創建類方法,則查找Inject註解的構造函數,看構造函數是否存在參數
  • 步驟3.1:若存在參數,則從步驟1開始依次初始化每個參數
  • 步驟3.2:若不存在參數,則直接初始化該類實例,一次依賴注入到此結束

也就說Dagger2會遞歸的提供依賴.

@Named和@Qulifier註解的使用

@Named

假設我們現在又有了新的需求,MainActivity中需要兩種布料,分別是紅布料和藍布料,但我們的MainModule類中只能提供紅布料,怎麼辦呢?
讀者可能會想:在MainModule類中再添加一個提供藍布料的方法不就行了:

    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
    @Provides
    public Cloth getBlueCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("藍色");
        return cloth;
    }

可問題就來了,Dagger2是通過返回值類型來確定的,當你需要紅布料時,它又怎麼知道哪個是紅布料呢?所以Dagger2爲我們提供@Named註解,它怎麼使用呢?它有一個value值,用來標識這個方法是給誰用的.修改我們的代碼:

    @Provides
    @Named("red")
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
    @Provides
    @Named("blue")
    public Cloth getBlueCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("藍色");
        return cloth;
    }

我們在getRedCloth方法上使用@Named("red")表明此方法返回的是紅布料,同理,在getBlueCloth方法上使用@Named("blue")表明此方法返回的是藍布料,接下我們只要在MainActivity中的布料字段上同樣使用@Named註解,就可以一一配對了.

public class MainActivity extends AppCompatActivity {
    ...
    @Inject
    @Named("red")
    Cloth redCloth;
    @Inject
    @Named("blue")
    Cloth blueCloth;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        tv.setText("我現在有" + redCloth + "和" + blueCloth );
    }
}

redCloth上用@Named("red")標記後,他就會對應Module中對應的方法.

結果

dagger2demo_4.png

@Qulifier

@Qulifier功能和@Named一樣,並且@Named就是繼承@Qulifier的,我們要怎麼使用@Qulifier註解呢?答案就是自定義一個註解:

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface RedCloth {
}

有了這個註解,我們就可以用它在替換掉上面的@Named("red"),效果是一樣的.讀者可以親自試一試.
而且這兩個註解還能使用在依賴參數上,比如這個:

    @Provides
    public Clothes getClothes(@Named("blue") Cloth cloth){
        return new Clothes(cloth);
    }

效果和上面說明的一樣,進入這個方法的cloth由上面有@Named("blue")的方法提供

@Singleton和@Scope的使用

@Singleton

假設現在MainActivity中需要依賴Clothes和Cloth,我們在MainModule中提供這兩個類的提供方法:

@Module
public class MainModule {

    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
    
    @Provides
    public Clothes getClothes(Cloth cloth){
        return new Clothes(cloth);
    }
}

接着在MainActivity中聲明:

public class MainActivity extends AppCompatActivity {
    private TextView tv;
    @Inject
    Cloth redCloth;

    @Inject
    Clothes clothes;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);

        MainComponent build = DaggerMainComponent.builder().mainModule(new MainModule()).build();
        build.inject(this);
        tv.setText("redCloth=clothes中的cloth嗎?:" + (redCloth == clothes.getCloth()));
    }
}

運行結果:

dagger2demo_5.png

你會發現,MainActivity中的Cloth對象和Clothes中的Cloth對象並不是同一個對象,注入過程中,對cloth注入時會調用一次getRedCloth方法,創建了一個Cloth對象;注入Clothes時又會調用一次getRedCloth方法,這時又會創建一個Cloth對象,所以纔會出現上面的結果.但是如果需要MainActivity中的Cloth對象和Clothes中的Cloth對象是同一個對象又要怎麼辦呢?Dagger2爲我們提供了@Singleton註解,和名字一樣,這個註解的作用就是聲明單例模式,我們先看看它怎麼使用,下面再講原理.
首先,在getRedCloth方法上添加該註解:

    @Singleton
    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }

再在MainComponent接口上添加該註解:

@Singleton
@Component(modules=MainModule.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

我們看看運行結果:

dagger2demo_6.png

有沒有發現,MainActivity中的Cloth對象和Clothes中的Cloth對象是同一個對象了,是不是很神奇!

@Scope

@Singleton是怎麼實現的呢?我們先看看@Scope註解,弄懂它,@Singleton你也就會明白了,下面我們就來分析分析
顧名思義,@Scope就是用來聲明作用範圍的.@Scope@Qulifier一樣,需要我們自定義註解才能使用,我們先自定義一個註解:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface PerActivity {
}

這個註解有什麼用呢?答案就是聲明作用範圍,當我們將這個註解使用在Module類中的Provide方法上時,就是聲明這個Provide方法是在PerActivity作用範圍內的,並且當一個Component要引用這個Module時,必須也要聲明這個Component是PerActivity作用範圍內的,否則就會報錯,聲明方法也很簡單,就是在Component接口上使用這個註解.但是我們聲明這個作用範圍又有什麼用呢?原來Dagger2有這樣一個機制:在同一個作用範圍內,Provide方法提供的依賴對象就會變成單例,也就是說依賴需求方不管依賴幾次Provide方法提供的依賴對象,Dagger2都只會調用一次這個方法.就和上面那個例子一樣,正常情況下,在注入MainActivity中的Cloth對象時會調用一次getRedCloth方法,注入Clothes對象時因爲依賴Cloth對象,所以又會調用一次getRedCloth方法,導致這兩個Cloth對象並不是同一個實例.但是我們給它聲明作用範圍後,這兩次對Cloth的依賴只會調用一次getRedCloth方法,這樣這兩個Cloth對象就是同一實例了,這樣就保證了在給MainActivity注入時,所有聲明的Cloth依賴都是指向同一個實例.(注意:只有Module類中聲明瞭作用範圍的Provide方法才能實現單例,沒聲明的方法就不是單例的)
查看源碼你會發現Singleton其實是繼承@Scope註解的,所以你知道了Singleton是怎麼實現單例模式的吧.
可能有些讀者可能會問,Dagger2既然有了Singleton爲什麼還要我們自定義PerActivity註解?這就涉及到代碼可讀性了,當依賴需求方是Activity時,我們可以自定義一個PerActivity註解,當依賴需求方是Fragment時,我們又可以自定義一個PerFragment註解,這樣我們就能清楚的區分依賴對象的提供目標了
那我們通過構造函數提供依賴的方式又要怎麼聲明作用範圍呢?答案就是在類名上使用註解標明,切記不要在構造函數上用註解標明,這樣是無效的.
讀者可以試試用PerActivity註解代替上面例子中的Singleton註解,你會發現效果是一樣的

注意注意注意:單例是在同一個Component實例提供依賴的前提下才有效的,不同的Component實例只能通過Component依賴才能實現單例.也就是說,你雖然在兩個Component接口上都添加了PerActivity註解,但是這兩個Component提供依賴時是沒有聯繫的,他們只能在各自的範圍內實現單例.(下一個例子會體現到)

組件依賴dependencies的使用

在實際開發中,我們經常會使用到工具類,工具類一般在整個App的生命週期內都是單例的,我們現在給我們的Demo添加一個工具類ClothHandler:

public class ClothHandler {
    public Clothes handle(Cloth cloth){
        return new Clothes(cloth);
    }
}

它的功能就是將cloth加工成clothes,假設我們現在有兩個Activity中都要使用該工具類,我們要怎麼使用Dagger2幫我們注入呢?
我們先用上面所學的方法試試,先在MainModule中添加提供方法:

@Module
public class MainModule {

    @PerActivity
    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
    
    @PerActivity
    @Provides
    public ClothHandler getClothHandler(){
        return new ClothHandler();
    }
}

再在MainActivity中聲明依賴:

public class MainActivity extends AppCompatActivity {
    private TextView tv;
    @Inject
    Cloth redCloth;

    @Inject
    ClothHandler clothHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);

        MainComponent build = DaggerMainComponent.builder().mainModule(new MainModule()).build();
        build.inject(this);
        tv.setText("紅布料加工後變成了" + clothHandler.handle(redCloth) + "\nclothHandler地址:" + clothHandler);
    }
    //在佈局文件中聲明的點擊方法
    public void onClick(View v){
        Intent intent = new Intent(this,SecondActivity.class);
        startActivity(intent);
    }
}

同理在書寫第二個Activity,併爲它書寫Module類Component接口:

public class SecondActivity extends AppCompatActivity {
    private TextView tv;

    @Inject
    Cloth blueCloth;
    @Inject
    ClothHandler clothHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        tv = (TextView) findViewById(R.id.tv);
        SecondComponent component = DaggerSecondComponent.builder().secondModule(new SecondModule()).build();
        component.inject(this);
        tv.setText("藍布料加工後變成了" + clothHandler.handle(blueCloth) + "\nclothHandler地址:" + clothHandler);
    }
}

@Module
public class SecondModule {

    @PerActivity
    @Provides
    public Cloth getBlueCloth(){
        Cloth cloth = new Cloth();
        cloth.setColor("藍色");
        return cloth;
    }

    @PerActivity
    @Provides
    public ClothHandler getClothHandler(){
        return new ClothHandler();
    }
}

@PerActivity
@Component(modules = SecondModule.class)
public interface SecondComponent {
    void inject(SecondActivity secondActivity);
}

我們來看看結果:

dagger2demo_7.png

你會發現,雖然我們成功的將ClothHandler注入到了這兩個Activity中,但是你會發現,這兩個Activity中的ClothHandler實例不是一樣的(驗證了上面那個結論),並且我們發現這種注入方式要在每一個Module中都要提供getClothHandler方法, 假如有20個Activity都需要用到ClothHandler,那我們都這樣寫,不就代碼重複了嗎.並且我們還要實現單例,怎麼辦呢?Dagger2很貼心的爲我們提供了Component依賴,就能完美的解決這個問題.
在面向對象的思想中,我們碰到這種情況一般都要抽取父類,Dagger2也是用的這種思想,我們先創建一個BaseModule,用來提供工具類:

@Module
public class BaseModule {
    
    @Singleton //單例
    @Provides
    public ClothHandler getClothHandler(){
        return new ClothHandler();
    }
}

在創建一個BaseComponent接口:


@Singleton //對應Module中聲明的單例
@Component(modules = BaseModule.class)
public interface BaseComponent {
    ClothHandler getClothHandler();
}

嗯?這個Component怎麼有點不一樣,怎麼沒有inject方法呢?上面講過,我們通過inject方法依賴需求方實例送到Component中,從而幫助依賴需求方實現依賴,但是我們這個BaseComponent是給其他Component提供依賴的,所以我們就可以不用inject方法,但是BaseComponent中多了一個getClothHandler方法,它的返回值是ClothHandler對象,這個方法有什麼用呢?它的作用就是告訴依賴於BaseComponent的Component,BaseComponent能爲你們提供ClothHandler對象,如果沒有這個方法,BaseComponent就不能提供ClothHandler對象(這個提供規則和上面的依賴規則相同,可以實現單例).既然有了BaseComponent,那我們就可在其它Component中依賴它了.我們刪除MainModule和SecondModule中的getClothHandler方法:

@Module
public class MainModule {

    @PerActivity
    @Provides
    public Cloth getRedCloth() {
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }

}
@Module
public class SecondModule {

    @PerActivity
    @Provides
    public Cloth getBlueCloth(){
        Cloth cloth = new Cloth();
        cloth.setColor("藍色");
        return cloth;
    }

}

接下來在MainComponent和SecondComponent中聲明依賴,就要用到@Component中的dependencies屬性了:

@PerActivity
@Component(modules=MainModule.class,dependencies = BaseComponent.class)
public interface MainComponent {
    void inject(MainActivity mainActivity);
}

@PerActivity
@Component(modules = SecondModule.class,dependencies = BaseComponent.class)
public interface SecondComponent {
    void inject(SecondActivity secondActivity);
}

下面我們用Android Studio中build菜單下的Rebuild Object選項後,你會發現創建MainComponent和SecondComponent實例時多了一個baseComponent方法:

dagger2demo_8.png

這個方法需要我們傳入一個BaseComponent實例,原因很簡單,MainComponent和SecondComponent既然依賴BaseComponent,肯定需要你傳入一個BaseComponent實例給它,它才能從BaseComponent實例中獲取到它需要的對象嘛.但是需要注意的是,如果要MainComponent和SecondComponent依賴到的對象是同一個的話(也就是單例),創建它們是傳入的BaseComponent實例也必須是同一個,上面說過,不同的Component實例是無法提供相同的依賴實例的,因爲它們之間是沒有聯繫的.這樣的話,我們就需要在MainActivity和SecondActivity中能獲取到同一個BaseComponent實例,怎麼樣能實現呢?很多人一開始都會想到用靜態工廠,這種方法可行,但是我們一般都會自定義一個Application類,用它來提供BaseComponent實例,因爲在整個App生命週期內都只有一個Application實例,所以其中的BaseComponent實例也不會變.我們自定義一個MyApplication類

public class MyApplication extends Application {
    private BaseComponent baseComponent;
    @Override
    public void onCreate() {
        super.onCreate();
        baseComponent = DaggerBaseComponent.builder().baseModule(new BaseModule()).build();
    }

    public BaseComponent getBaseComponent() {
        return baseComponent;
    }
}

我們在onCreate方法中創建BaseComponent實例,並對外提供獲取方法.
這種方式還有一種好處,就是當我們在BaseModule中需要用到Application實例時,我們就可以在創建BaseModule時傳入this.
接下來在AndroidManifest.xml中聲明我們新建的MyApplication:

<?xml version="1.0" encoding="utf-8"?>
<manifest package="cn.izouxiang.dagger2demo2"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <activity android:name=".SecondActivity">
        </activity>
    </application>

</manifest>

接下來修改MainActivity和SecondActivity中的代碼:

public class MainActivity extends AppCompatActivity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        MainComponent build = DaggerMainComponent
                .builder()
                .baseComponent(((MyApplication)getApplication()).getBaseComponent())
                .mainModule(new MainModule())
                .build();
        build.inject(this);
        tv.setText("紅布料加工後變成了" + clothHandler.handle(redCloth) + "\nclothHandler地址:" + clothHandler);
    }

    ...
}
public class SecondActivity extends AppCompatActivity {
   ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        SecondComponent component = DaggerSecondComponent
                .builder()
                .baseComponent(((MyApplication)getApplication()).getBaseComponent())
                .secondModule(new SecondModule())
                .build();
        component.inject(this);
        tv.setText("藍布料加工後變成了" + clothHandler.handle(blueCloth) + "\nclothHandler地址:" + clothHandler);
    }
}

運行結果:

dagger2demo_9.png

我們成功的將ClothHandler注入到了這兩個Activity中,並且還實現了單例.(注意:這裏能實現單例跟BaseComponent中聲明瞭@Singleton有很大關係,因爲BaseComponent都沒有單例的話,外部依賴它的Component就更不可能單例了).

@Subcomponent註解

@Subcomponent註解的功能和component依賴類似,但是使用方法有點不同,component依賴需要在被依賴的Component(下文中稱爲父組件)中暴露接口,沒有暴露接口的類型在依賴方Component(下文中稱爲子組件)是獲取不到的,但是通過@Subcomponent,子組件可以獲取到所有父組件能提供的類型,下面我們來看看@Subcomponent註解的使用方法:
先聲明一個SubMainComponent組件接口,這裏的聲明方式和最基本的Component接口聲明方式差別不大,只是要將接口上的@Component註解改爲@Subcomponent註解

@PerActivity
@Subcomponent(modules = MainModule.class)
public interface SubMainComponent {
    void inject(MainActivity activity);
}

這一步是重點,我們需要在父組件中聲明一個返回值爲子組件的方法,當子組件需要什麼Module時,就在該方法中添加該類型的參數

@Singleton
@Component(modules = BaseModule.class)
public interface BaseComponent {
    //這個是爲第二個Activity準備的,也就是dependencies依賴聲明的方式
    ClothHandler getClothHandler();

    //@Subcomponent使用的聲明方式,聲明一個返回值爲子組件的方法,子組件需要什麼Module,就在方法參數中添加什麼
    SubMainComponent getSubMainComponent(MainModule module);
}

最後修改MainActivity:還是先獲取到BaseComponent,再調用getSubMainComponent()方法,當中傳入SubMainComponent組件需要的MainModule,這樣我們就獲取到了繼承了BaseComponent組件的SubMainComponent組件,再調用一下inject方法完成注入就ok了

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    tv = (TextView) findViewById(R.id.tv);
    MyApplication application = (MyApplication) getApplication();
    application.getBaseComponent().getSubMainComponent(new MainModule()).inject(this);
    tv.setText("紅布料加工後變成了" + clothHandler.handle(redCloth) + "\nclothHandler地址:" + clothHandler);
}

最後查看結果

dagger2demo_10.png

這裏我們發現,雖然第一個Activity使用的是@subcomponent方式,第二個使用的是dependencies依賴的方式,但是ClothHandler還是實現了單例,出現這個原因我們上面也講到了,因爲這兩個Activity中用到的BaseComponent是同一個實例,因爲在BaseComponent中ClothHandler是單例的,那麼通過BaseComponent提供的ClothHandler的肯定也是單例的嘛
這裏總結一下@Subcomponent的使用:

  • 子組件的聲明方式由@Component改爲@Subcomponent
  • 在父組件中要聲明一個返回值爲子組件的方法,當子組件需要什麼Module時,就在該方法中添加該類型的參數

注意:用@Subcomponent註解聲明的Component是無法單獨使用的,想要獲取該Component實例必須經過其父組件

Lazy與Provider

Lazy和Provider都是用於包裝Container中需要被注入的類型,Lazy用於延遲加載,所謂的懶加載就是當你需要用到該依賴對象時,Dagger2才幫你去獲取一個;Provide用於強制重新加載,也就是每一要用到依賴對象時,Dagger2都會幫你依賴注入一次,下面我們來看個小例子:
修改MainModule類

@Module
public class MainModule {
    private static final String TAG = "MainModule";
    //注意:這裏沒有聲明作用域內單例
    @Provides
    public Cloth getRedCloth() {
        Log.d(TAG, "getRedCloth: ...");
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
    //注意:這裏沒有聲明作用域內單例
    @Provides
    public Shoe getShoe(){
        Log.d(TAG, "getShoe: ...");
        return new Shoe();
    }
}

再修改MainActivity類

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @Inject //Lazy聲明方式
    Lazy<Cloth> redCloth;
    @Inject //Provider聲明方式
    Provider<Shoe> shoe;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tv = (TextView) findViewById(R.id.tv);
        MyApplication application = (MyApplication) getApplication();
        //這裏雖然沒使用到BaseComponent,但是我們還是得傳入BaseComponent,不然Dagger2會報錯
        MainComponent component = DaggerMainComponent.builder().baseComponent(application.getBaseComponent()).mainModule(new MainModule()).build();
        component.inject(this);
        Log.d(TAG, "inject done ...");
        Log.d(TAG, "1 use redCloth instance ..");
        Log.d(TAG, "redCloth:" + redCloth.get());
        Log.d(TAG, "2 use redCloth instance ..");
        Log.d(TAG, "redCloth:" + redCloth.get());
        Log.d(TAG, "1 use shoe instance ..");
        Log.d(TAG, "shoe:" + shoe.get());
        Log.d(TAG, "2 use shoe instance ..");
        Log.d(TAG, "shoe:" + shoe.get());
    }

    public void onClick(View v) {
        Intent intent = new Intent(this, SecondActivity.class);
        startActivity(intent);
    }
}

其中Lazy和Provider的使用方法就是使用該類作爲字段,泛型類型就是你要依賴的類型,當我們要獲取該對象時,使用該類的get方法就行了.
下面我們來看看運行結果

dagger2demo_11.png

你會發現,cloth和shoe都是使用時纔去調用module中的方法,不同的是,cloth只會調用一次,而shoe每次都會調用module中的方法,即對shoe重新注入,這也就是Lazy和Provider的區別.
上面的MainModule中是沒有聲明作用域內單例的,現在我們聲明一下再看看結果有沒有什麼變化:
修改MainModule

@Module
public class MainModule {
    private static final String TAG = "MainModule";
    @PerActivity //這裏聲明作用域內單例
    @Provides
    public Cloth getRedCloth() {
        Log.d(TAG, "getRedCloth: ...");
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }

    @PerActivity //這裏聲明作用域內單例
    @Provides
    public Shoe getShoe(){
        Log.d(TAG, "getShoe: ...");
        return new Shoe();
    }
}

查看結果:

dagger2demo_12.png

發現,聲明單例後,使用shoe時也不會每次都去調用module中的方法了,這是因爲Provider的作用是每次使用時都對依賴對象重新注入,但是Shoe在Component中是單例的,所以每次注入的都是同一個實例,所以只會調用一次module中的方法.

Component的生命週期

一般情況下我們都是在Activity的onCreate方法中創建Component實例,再調用inject方法完成依賴.所以Component依賴可以分爲三個過程:

  1. 創建Component實例
    MainComponent component = DaggerMainComponent
                .builder()
                .baseComponent(((MyApplication)getApplication()).getBaseComponent())
                .mainModule(new MainModule())
                .build();
    
  2. 調用inject方法
    component.inject(this);
    
    調用完這個方法整個依賴就完成了.
  3. Component實例被銷燬
    onCreate()方法調用完成後,Component實例就會因爲沒有被引用而被垃圾回收器回收.其中傳入給Component實例的Module實例也會一同被回收,這也就能說明不同的Component實例之間是沒有聯繫的(Component依賴除外).這裏需要注意的是,使用Lazy和Provider時,與該依賴對象有關的Module實例會被Lazy和Provider引用,所以該Module實例不會被垃圾回收器回收

總結:

  • 至此,Dagger2基礎已講完,對於Dagger2在項目中的使用方法,可以參考github上的開源項目.希望此篇文章能夠對你有所幫助!
  • 本篇文章是筆者用來記錄自己對Dagger2的理解的,如果當中有錯誤,還請賜教,以便筆者糾正.
  • 能夠書寫本篇文章,還得多虧了各位大神的blog,正因爲各位大神的分享精神,才讓我們這種小菜鳥能夠成長.此篇文章分享出來的目的也就是爲了傳承這種精神

最後我們引用一下Dagger2 Scope 註解能保證依賴在 component 生命週期內的單例性嗎?中的注意事項:

  • component 的 inject 函數不要聲明基類參數;
  • Scope 註解必須用在 module 的 provide 方法上,否則並不能達到局部單例的效果;
  • 如果 module 的 provide 方法使用了 scope 註解,那麼 component 就必須使用同一個註解,否則編譯會失敗;
  • 如果 module 的 provide 方法沒有使用 scope 註解,那麼 component 和 module 是否加註解都無關緊要,可以通過編譯,但是沒有局部單例效果;
  • 對於直接使用 @Inject 構造函數的依賴,如果把 scope 註解放到它的類上,而不是構造函數上,就能達到局部單例的效果了;

筆者再總結

  • 被依賴的Component能提供某個對象時,一定要在接口中聲明以該對象爲返回值的方法(也就是暴露接口).這樣依賴它的Component才能獲取到這種對象.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章