Dagger2 入門,以初學者角度.

Dagger2 入門


2016-12-21 更新:添加@Subcomponent注解以及Lazy與Provider的使用,本文基本完結!如果有好的建議請?zhí)岢?感謝大家的支持,謝謝

博客地址:Dagger2 入門,以初學者角度.

[TOC]

依賴注入

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

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

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

Dagger2介紹

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

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

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

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

通俗上來講就好比你現(xiàn)在需要一件衣服,自己做太麻煩了,你就去商店買,你跟商店老板說明你想要購買的類型后,商店老板就會在自己的衣服供應商中查找有沒有你所說的類型,有就將它賣給你.其中你就對應上面所說的依賴需求方實例,你只要說明你需要什么,商店老板則對應Component實現(xiàn)類,負責滿足別人的需求,而衣服供應商則對應Module類,他負責生產(chǎn)衣服.也許這里有點繞,但經(jīng)過下面的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類

現(xiàn)在的需求是MainActivity中需要使用到Cloth對象,所以我們要為MainActivity書寫一個Module類用來提供Cloth對象,相當于創(chuàng)建了一個提供商

@Module
public class MainModule {

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

嗯?怎么多了兩個注解?這兩個注解有什么用呢?
注解是Dagger2中的關鍵,編寫Module類時要在該類上聲明@Module以表明該類是Module類,這樣Dagger2才能識別,那@Provides又是干嘛的呢?它的作用是聲明Module類中哪些方法是用來提供依賴對象的,當Component類需要依賴對象時,他就會根據(jù)返回值的類型來在有@Provides注解的方法中選擇調(diào)用哪個方法.在一個方法上聲明@Provides注解,就相當于創(chuàng)建了一條生產(chǎn)線,這條生產(chǎn)線的產(chǎn)物就是方法的返回值類型.有了這條生產(chǎn)線,供應商就能提供這種類型的商品了,當商店老板發(fā)現(xiàn)有人需要這種類型的商品時,供應商就可以提供給他了

書寫Component接口

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

和Module類一樣,Component類也是需要注解聲明的,那個注解就是@Component,但是@Component注解的作用可不是單單用來聲明Component類,他還有更強大的功能,@Component注解有modules和dependencies兩個屬性,這兩個屬性的類型都是Class數(shù)組,modules的作用就是聲明該Component含有哪幾個Module,當Component需要某個依賴對象時,就會通過這些Module類中對應的方法獲取依賴對象,MainComponent中只包含MainModule,所以令modules=MainModule.class,相當于供應商和商店老板確定合作關系的合同.而dependencies屬性則是聲明Component類的依賴關系,這個下面再詳講.
接口中那個方法又是干嘛用的呢?
我們現(xiàn)在只是聲明了Component類,但我們要怎么將Component類和依賴需求方對象聯(lián)合起來呢?答案就是通過這個inject方法,這個方法可以將依賴需求方對象送到Component類中,Component類就會根據(jù)依賴需求方對象中聲明的依賴關系來注入依賴需求方對象中所需要的對象,本Demo中MainActivity中需要Cloth對象,所以我們通過inject方法將MainActivity實例傳入到MainComponent中,MainComponent就會從MainModule中的getCloth方法獲取Cloth實例,并將該實例賦值給MainActivity中的cloth字段.相當于你去商店的道路,沒有這條路,你就無法去商店和老板說明你所需要的東西.但是這里需要注意的是,inject方法的參數(shù)不能用父類來接收,例如本Demo中,如果inject的參數(shù)是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("我現(xiàn)在有" + cloth);
    }
}

上面代碼中有兩處關鍵:

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

結果

dagger2demo_1.jpeg

另一種方法

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

創(chuàng)建依賴類Shoe

我們又創(chuàng)建一個依賴類Shoe

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

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

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

在MainActivity中聲明Shoe依賴

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

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

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

結果

dagger2demo_2.jpeg

注意

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

復雜一點的情況

我們創(chuàng)建的這些依賴類都不用依賴于其它依賴類,但是如果需要依賴于其它依賴類又要怎么弄呢?

創(chuàng)建依賴類Clothes

我們又來創(chuàng)建一個衣服類Clothes,制作衣服時需要布料,所以我們在創(chuàng)建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類中增加提供方法

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

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

直接在方法中創(chuàng)建一個Cloth不就得了,但是你有沒有發(fā)現(xiàn),創(chuàng)建Cloth的代碼已經(jīng)在getCloth方法中有了,我們能不能用getCloth方法中創(chuàng)建的Cloth實例來創(chuàng)建Clothes實例呢?
Dagger2提供了這樣的功能,我們只要在getClothes方法中添加Cloth參數(shù),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("我現(xiàn)在有" + cloth + "和" + shoe + "和" + clothes);
    }
}

結果

dagger2demo_3.jpeg

依賴總結

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

我們有兩種方式可以提供依賴,一個是注解了@Inject的構造方法,一個是在Module里提供的依賴,那么Dagger2是怎么選擇依賴提供的呢,規(guī)則是這樣的:

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

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

@Named和@Qulifier注解的使用

@Named

假設我們現(xiàn)在又有了新的需求,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("我現(xiàn)在有" + redCloth + "和" + blueCloth );
    }
}

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

結果

dagger2demo_4.png

@Qulifier

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

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

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

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

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

@Singleton和@Scope的使用

@Singleton

假設現(xiàn)在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

你會發(fā)現(xiàn),MainActivity中的Cloth對象和Clothes中的Cloth對象并不是同一個對象,注入過程中,對cloth注入時會調(diào)用一次getRedCloth方法,創(chuàng)建了一個Cloth對象;注入Clothes時又會調(diào)用一次getRedCloth方法,這時又會創(chuàng)建一個Cloth對象,所以才會出現(xiàn)上面的結果.但是如果需要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

有沒有發(fā)現(xiàn),MainActivity中的Cloth對象和Clothes中的Cloth對象是同一個對象了,是不是很神奇!

@Scope

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

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

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

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

組件依賴dependencies的使用

在實際開發(fā)中,我們經(jīng)常會使用到工具類,工具類一般在整個App的生命周期內(nèi)都是單例的,我們現(xiàn)在給我們的Demo添加一個工具類ClothHandler:

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

它的功能就是將cloth加工成clothes,假設我們現(xiàn)在有兩個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

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

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

在創(chuàng)建一個BaseComponent接口:


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

嗯?這個Component怎么有點不一樣,怎么沒有inject方法呢?上面講過,我們通過inject方法依賴需求方實例送到Component中,從而幫助依賴需求方實現(xiàn)依賴,但是我們這個BaseComponent是給其他Component提供依賴的,所以我們就可以不用inject方法,但是BaseComponent中多了一個getClothHandler方法,它的返回值是ClothHandler對象,這個方法有什么用呢?它的作用就是告訴依賴于BaseComponent的Component,BaseComponent能為你們提供ClothHandler對象,如果沒有這個方法,BaseComponent就不能提供ClothHandler對象(這個提供規(guī)則和上面的依賴規(guī)則相同,可以實現(xiàn)單例).既然有了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選項后,你會發(fā)現(xiàn)創(chuàng)建MainComponent和SecondComponent實例時多了一個baseComponent方法:

dagger2demo_8.png

這個方法需要我們傳入一個BaseComponent實例,原因很簡單,MainComponent和SecondComponent既然依賴BaseComponent,肯定需要你傳入一個BaseComponent實例給它,它才能從BaseComponent實例中獲取到它需要的對象嘛.但是需要注意的是,如果要MainComponent和SecondComponent依賴到的對象是同一個的話(也就是單例),創(chuàng)建它們是傳入的BaseComponent實例也必須是同一個,上面說過,不同的Component實例是無法提供相同的依賴實例的,因為它們之間是沒有聯(lián)系的.這樣的話,我們就需要在MainActivity和SecondActivity中能獲取到同一個BaseComponent實例,怎么樣能實現(xiàn)呢?很多人一開始都會想到用靜態(tài)工廠,這種方法可行,但是我們一般都會自定義一個Application類,用它來提供BaseComponent實例,因為在整個App生命周期內(nèi)都只有一個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方法中創(chuàng)建BaseComponent實例,并對外提供獲取方法.
這種方式還有一種好處,就是當我們在BaseModule中需要用到Application實例時,我們就可以在創(chuàng)建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中,并且還實現(xiàn)了單例.(注意:這里能實現(xiàn)單例跟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時,就在該方法中添加該類型的參數(shù)

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

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

最后修改MainActivity:還是先獲取到BaseComponent,再調(diào)用getSubMainComponent()方法,當中傳入SubMainComponent組件需要的MainModule,這樣我們就獲取到了繼承了BaseComponent組件的SubMainComponent組件,再調(diào)用一下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

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

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

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

Lazy與Provider

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

@Module
public class MainModule {
    private static final String TAG = "MainModule";
    //注意:這里沒有聲明作用域內(nèi)單例
    @Provides
    public Cloth getRedCloth() {
        Log.d(TAG, "getRedCloth: ...");
        Cloth cloth = new Cloth();
        cloth.setColor("紅色");
        return cloth;
    }
    //注意:這里沒有聲明作用域內(nèi)單例
    @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

你會發(fā)現(xiàn),cloth和shoe都是使用時才去調(diào)用module中的方法,不同的是,cloth只會調(diào)用一次,而shoe每次都會調(diào)用module中的方法,即對shoe重新注入,這也就是Lazy和Provider的區(qū)別.
上面的MainModule中是沒有聲明作用域內(nèi)單例的,現(xiàn)在我們聲明一下再看看結果有沒有什么變化:
修改MainModule

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

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

查看結果:

dagger2demo_12.png

發(fā)現(xiàn),聲明單例后,使用shoe時也不會每次都去調(diào)用module中的方法了,這是因為Provider的作用是每次使用時都對依賴對象重新注入,但是Shoe在Component中是單例的,所以每次注入的都是同一個實例,所以只會調(diào)用一次module中的方法.

Component的生命周期

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

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

總結:

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

最后我們引用一下Dagger2 Scope 注解能保證依賴在 component 生命周期內(nèi)的單例性嗎?中的注意事項:

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

筆者再總結

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

聲明:

本文為筆者原創(chuàng)文章,轉載需說明!

參考文章:

依賴注入神器:Dagger2詳解系列
Dagger2 Scope 注解能保證依賴在 component 生命周期內(nèi)的單例性嗎?
GeekNews
Dagger2圖文完全教程
Android常用開源工具(2)-Dagger2進階

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內(nèi)容