1. 程式人生 > >輕鬆學,聽說你還沒有搞懂 Dagger2

輕鬆學,聽說你還沒有搞懂 Dagger2

Dagger2 確實比較難學,我想每個開發者學習的時候總是經歷了一番痛苦的掙扎過程,於是就有了所謂的從入門到放棄之類的玩笑,當然不排除基礎好的同學能夠一眼看穿。本文的目的嘗試用比較容易理解的角度去解釋 Dagger2 這樣東西。

Dagger2 是有門檻的,這樣不同水平能力的開發者去學習這一塊的時候,感受到的壓力是不一樣的。

我個人總結了大家在學習 Dagger2 時,為什麼感覺難於理解的一些原因。

  1. 對於 Java 註解內容不熟悉。
  2. 對於依賴注入手段不熟悉。
  3. 對於 Java 反射不熟悉。
  4. 對於 Dagger2 與其它開源庫的使用方法的不同之處,沒有一個感性的認知。
  5. 對於 Dagger2 中極個別的概念理解不夠。
  6. 對於 Dagger2 的用途與意義心生迷惑。

其實以上幾點,都可以歸類到基礎技能不紮實這個範疇內,但正如我所說的,學習 Dagger2 時開發者的水平是不一樣的,所以困擾他們的原因就不一樣。下面,我針對這些情況,一一給出自己的建議。

對於 Java 註解不熟悉

這一部分的開發者基礎知識確實薄弱,那麼怎麼辦呢?當然是學習了。就算不為 Dagger2,註解的知識內容也應該好好值得學習,雖然在平常開發中,我們自己編寫註解的機會很少,但是我們運用第三方開源庫的時候,應該會經常看見註解的身影,所以熟悉註解不是為了自己編寫註解程式碼,而是為了開發過程中更加高效從容而已。

如果,對 Java 註解一無所知,我可以給大家一個感性的認知。

一般,我們評價某人會說,這是一個好人、壞人、男神、女神、大神、單身狗等等,這是我們人為貼得標籤,這些標籤有助於我們自己或者其他人去獲取被評價的人的基本資訊。

而在 Java 軟體開發中,我們也可以給某些類,某些欄位貼上作用類似的標籤,這種標籤的名字就叫做註解,只不過這種標籤是給程式碼看的。

這裡寫圖片描述

標籤只對特定的人起作用,比如小張被人貼了一個小氣鬼的標籤,所以小紅認為小張是一個小氣鬼,但是小張本人不會因為這個標籤而改變自己變得不是小張,也許本質上小張是個大方的人。

所以,註解本身也不會影響程式碼本身的執行,它只會針對特定的程式碼起到一定的用處,用來處理註解的程式碼被稱作 APT(Annotation Processing Tool)。

對依賴注入手段不熟悉

這一塊而言,如果讓很多人慌張的原因,我覺得可能是依賴注入這個詞過於學術化了。而從小到大,10 多年的應試教育讓絕大部分的同學對於這些枯燥無味的概念產生了恐懼與絕望。其實,沒有那麼誇張的,不要被這些東西嚇倒。

因為,Java 學習的時候,我們一直寫這樣的程式碼。

class B{}


class A {
    B b;

    public A() {
        b = new B();
    }
}

這樣的程式碼,一點問題都沒有,類 A 中有一個成員變數 b,b 的型別是類 B。所以,在軟體開發中,可以稱 A 依賴 B,B 是 A 的依賴,顯然,A 可以依賴很多東西,B 也可以依賴很多東西。

通俗地講,依賴這個概念也沒有什麼神奇的,只是描述了一種需求關係。

我們再來看一種情況,現在,業務需要,程式碼越來越複雜。

class B{}

class C{
    int d;
    public C (int value) {
        this.d = value;
    }
}


class A {
    B b;
    C c;

    public A() {
        b = new B();
        c = new C(3);
    }
}

現在,A 有了一個新的依賴 C。不過,由於業務的演進,C 這個類經常發生變化,最明顯的變化就是它的構造方法經常變動。

class C{
    int d;
    String e;
    public C (String value) {
        this.e = value;
    }
}


class A {
    B b;
    C c;

    public A() {
        b = new B();
        //c = new C(3);
        c = new C("hello");
    }
}

C 變動的時候,由於 A 依賴於它,A 不得不修改自己的程式碼。但是,事情還沒有完。C 還會變動,C 把 B 也帶壞了節奏。

class B{
    int value;

    public B(int value) {
        this.value = value;
    }

}

class C{
    int d;
    String e;
    public C (int index,String value) {
        this.d = index;
        this.e = value;
    }
}


class A {
    B b;
    C c;

    public A() {
        b = new B(110);
//      b = new B();
        //c = new C(3);
//      c = new C("hello");
        c = new C(12,"hello");
    }
}

可以想像的是,只要 B 或者 C 變動一次,A 就可能需要修改自己的程式碼,用專業術語描繪就是A 與依賴模組太過於耦合,這個可是犯了軟體設計的大罪,

我們再可以想像一下,A 是領導,B 和 C 是小兵,如果因為 B 和 C 自身的原因,導致領導 A 一次次地改變自己,那麼以現在流行的話來說就是,“你良心不會痛嗎?”。所以我們需要的就是進行一些變化來進行解耦,也就是解除這種耦合的關係。讓 A 不再關心 B 和 C 的變化,而只要關心自身就好了。

class A {
    B b;
    C c;

    public A(B b, C c) {
        this.b = b;
        this.c = c;
    }

}  

在上面程式碼中,A 不再直接建立 B 與 C,它把依賴的例項的權力移交到了外部,所以無論 B 和 C 怎麼變化,都不再影響 A 了。這種例項化依賴的權力移交模式被稱為控制反轉(IoC),而這種通過將依賴從構造方法中傳入的手段就是被傳的神乎其乎的依賴注入(DI)。其實,本質上也沒有什麼神奇的地方,只是起了一個高大上的名字而已,好比東北的馬麗,在國際化上的大舞臺,宣稱自己是來自神祕東方的 Marry 一樣。

依賴注入有 3 種表現形式。
構造方法注入

class A {
    B b;
    C c;

    public A(B b, C c) {
        this.b = b;
        this.c = c;
    }

} 

Setter 注入

class A {
    B b;
    C c;


    public void setB(B b) {
        this.b = b;
    }


    public void setC(C c) {
        this.c = c;
    }

}

介面注入

interface Setter {
    void setB(B b);
    void setC(C c);
}


class A implements Setter{
    B b;
    C c;

    public void setB(B b) {
        this.b = b;
    }


    public void setC(C c) {
        this.c = c;
    }

}

大家肯定會想,依賴注入的引進,使得需求方不需要例項化依賴,但總得有地方去例項化這些依賴啊。確實,依賴注入引進了第三方,你可以稱它為 IoC 容器,也可以稱它為注射器(injector),為了便於理解,我們之後都有注射器來指代吧,通過注射器可以將依賴以上面 3 種注入方式之一注入到需求方。

這裡寫圖片描述

病人需要的是藥水,所以病人是需求者,藥水是病人的依賴,注射器把藥水注射給病人。

而 Dagger2 就是一個依賴注入框架,你也可以想像它是一位非常智慧化的服務員,用來處理大量的顧客的各種訂餐需求,然後針對不同的選單提供給不同的顧客不同型別的餐具。

對於 Java 反射不熟悉

對於這一塊不熟悉的同學同樣是基礎知識太薄弱,需要補強。

相對於正常流程開發,Java 反射是非常規化手段。如果正常流程開發是司機駕駛一輛汽車,那麼反射的運用就是採用無人駕駛的手段。

Dagger2 中也應用了反射,不過開發者本身不需要運用反射,Dagger2 是自身框架通過反射處理註解。

Dagger2 與其它開源庫略有不同

開源軟體的出現,大大造福了程式設計師,所以,大家都說不要重複創造輪子

但是,我個人一直認為,不重複創造輪子,不代表可以不去深入瞭解這些輪子。

我把 Android 開發中所應用到的開源庫當作武裝

武裝與兩部分構成,武器裝備

那麼,在 Android 中什麼樣的庫可以當作是武器呢?什麼樣的庫可以當作是裝備呢?

大家想一下,武器什麼用途?戰鬥進行中,用來殺敵的

裝備呢?戰鬥開始時,就要穿上或者安裝好的物件。
這裡寫圖片描述

刀、槍、棍、棒是武器,盔甲是裝備。
武器拿來就用,盔甲等卻要在開始戰鬥前就裝備上。

Java 軟體程式碼是在虛擬機器中執行的,所以在這裡可以把 jvm 當作戰場。

Piccso、Logger、sweet-alert-dialog 等等,這些開源庫都是在程式執行過程中拿來就用的。

而 GreenDao、Butterknife、Dagger2 這些因為涉及到了反射處理,而反射處理相對於正常開發速度很慢,所以它們通常在編譯時產生一些新的程式碼,然後才能在程式執行過程中使用,也就是說它們都把反射處理移動到編譯器編譯程式碼時的階段,而程式執行時並不涉及到反射,這就是這些框架運用了反射技術,但是仍然高效的祕訣所在。

所以,Dagger2 會產生中間程式碼,不少同學應該會有迷惑,為什麼引進了 Dagger2 時,要先編譯一次程式碼,不然就會報錯。現在,可以解釋了,編譯程式碼是為了生成中間程式碼,然後在中間程式碼的基礎上按照正常的流程開發。

Dagger2 並非橫空出世

都說要站在巨人的肩膀上,Dagger2 其實也算站在巨人的肩膀上。

Dagger2 是一款依賴注入的框架,但依賴注入的框架有 ,所以 Dagger2 也並不算是一款新鮮事物,大家覺得新奇不過是因為對於依賴注入框架本身瞭解過少罷了。

Dagger2 是在 Dagger 的基礎上來的,Dagger 是由 Square 公司開發的,Dagger2 基於 Dagger 由 Google 公司開發並維護。
Square 是一家偉大的公司,Android 大神 JakeWoton 之前就在它任職,不久前才離職。而我們熟悉的 RxJava、Butterknife、Retrofit、OKHttp 等等都是 Square 提供的,外號 Square 全家桶。
這裡寫圖片描述

當然,Google 公司更是一家偉大的公司,這個無需多言。

最後,有個重要的地方就是 Dagger2 是基於註解開發的,而 Dagger2 中所涉及到的註解其實是基於 javax.inject 上開發的,它出自 JSR330
這裡寫圖片描述

JSR330 是規範,建議大家怎麼做,而 Dagger2 則實現了這個規範。

因此,對於普通開發者而言,學習 Dagger2 其實就是學習相關的註解的意義與用途。

Dagger2 的引進

Dagger2 是適應於 Java 和 Android 開發的依賴注入框架,記住得是它不僅僅對 Android 開發有效。

對於 Eclipse 開發而言,需要下載相應的 jar 包。

對於 AndroidStudio 開發而言,只需要在相應的 build.gradle 引入對應的依賴就好了。

如果你 AndroidStudio 的 gradle build tool 版本在 2.2 以上,直接在引進就好了

dependencies {
  compile 'com.google.dagger:dagger:2.4'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.4'
}

如果你的 gradle build tool 版本在 2.2 以下,則需要引進 apt 外掛。
首先需要在 Project 層級的 build.gradle 檔案中引入依賴

buildscript {
    repositories {
      mavenCentral()
    }
    dependencies {
        // replace with the current version of the Android plugin
        classpath 'com.android.tools.build:gradle:2.1.0'
        // the latest version of the android-apt plugin
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

然後在 Module 層級的 build.gradle 引入相應的外掛和依賴

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt'


dependencies {
     apt 'com.squareup.dagger:dagger-compiler:2.4'
     compile 'com.squareup.dagger:dagger:2.4'
     //java註解
     compile 'org.glassfish:javax.annotation:10.0-b28'
}

Dagger2 的基本概念

前面講到過 Dagger2 基於 JSR330 註解,在普通開發者視角中,就是這些註解構成了 Dagger2 的全部。

前面文章我提到過,註解如同標籤,給一個人貼標籤有助於自己去理解這個人,而給程式碼貼標籤,有助於 APT 程式去處理相應的程式碼,Dagger2 有自己的註解,而這些註解也有特定的意義,它們大體上都是為了實現依賴注入。

我們說依賴注入有 3 種手段:
- 構造方法注入
- Setter 注入
- 介面注入

但是,如果不借助於框架的話,我們就必須自己編寫相應的程式碼,這些程式碼充當了注射器的角色。

B b = new B(5);
C c = new C(110,"110");

A a = new A();
a.setB(b);
a.setC(c);

A 將內部的依賴 B 和 C 的例項化的權力移交到了外部,通過外部的注入。

Dagger2 這類依賴注入框架的出現進一步解放了我們的雙手,Dagger2 有一套自己的依賴注入機制,我們不再手動編寫注射器,而只要按照規則配置好相應的程式碼就好了,Dagger2 會自動幫我們生成注射器,然後在適當的時候進行依賴注入。

什麼意思呢?意思就是我們不需要呼叫 a.setB() 和 a.setC() 方法,只需對程式碼新增一些註解就好了。

class B{
    int value;
    @Inject
    public B(int value) {
        this.value = value;
    }

}

class C{
    int d;
    String e;
    @Inject
    public C (int index,String value) {
        this.d = index;
        this.e = value;
    }
}

class A {
    @Inject
    B b;
    @Inject
    C c;
}

看起來,不可思議,不是嗎?@Inject 是一個註解,只要按照 Dagger2 的配置,就能顛覆我們之前的編碼習慣。

但不管看起來怎麼神奇,任何事都有一個本質。

因此這個本質就是,Dagger2 是一個依賴注入框架,依賴注入的目的就是為了給需求方在合適的時候注入依賴。

對 Dagger2 學習過程如果感到不適與難以理解,回過頭來想想它的本質好了。

這裡寫圖片描述
Dagger2 的使命就是為了給需求者注射依賴。

@Inject 註解就如同一個標籤,或者說它是一個記號,它是給 Dagger2 看的。它運用的地方有兩處。

  1. @Inject 給一個類的相應的屬性做標記時,說明了它是一個依賴需求方,需要一些依賴。

  2. @Inject 給一個類的構造方法進行註解時,表明了它能提供依賴的能力。

就這樣,通過 @Inject 註解符號,就很容易標記依賴和它的需求方。但是,單單一個 @Inject 是不能讓 Dagger2 正常執行的。還需要另外一個註解配合。這個註解就是 @Component。

而 @Component 相當於聯絡紐帶,將 @inject 標記的需求方和依賴繫結起來,並建立了聯絡,而 Dagger2 在編譯程式碼時會依靠這種關係來進行對應的依賴注入。

@Inject 和 @Component

我們來編寫程式碼,驗證一下。

假設有這麼一個場景:

一個宅男,他喜歡在家玩遊戲,所以餓了的時候,他不想自己煮飯吃,也不願意下樓去餐廳,他選擇了外賣。

public class ZhaiNan {

    @Inject
    Baozi baozi;

    @Inject
    Noodle noodle;

    @Inject
    public ZhaiNan() {

    }

    public String eat() {
        StringBuilder sb = new StringBuilder();
        sb.append("我吃的是 ");
        if ( baozi != null ) {
            sb.append(baozi.toString());
        }

        if (noodle != null) {
            sb.append("  ");
            sb.append(noodle.toString());
        }
        return sb.toString();
    }
}

public class Baozi {

    @Inject
    public Baozi() {
    }

    @Override
    public String toString() {
        return "小籠包";
    }
}

public class Noodle {

    @Inject
    public Noodle() {
    }

    @Override
    public String toString() {
        return "麵條";
    }
}

上面程式碼可以看到,@Inject 註解的身影,需求方是 ZhaiNan 這個類,而 Baozi 和 Noodle 是它的依賴。前面說過,光有 @Inject 的話還不行,需要 @Component 配合。

@Component 怎麼使用呢?

很簡單,它只需要註解在一個介面上就好了。

@Component()
public interface Platform {
    ZhaiNan waimai();
}

Platform 是一個介面,它代表著外賣平臺,它內部有一個 waimai() 的方法,返回 ZhaiNan 的型別。

這個介面特別的地方就是它的方法中的返回型別。如果一個方法返回了一個型別,那麼其實也算是一種依賴的提供,我們可以在後續的程式碼中感受。

既然是介面,那麼它就需要實現類,但是 Dagger2 會自動幫我們生成一個實現類,前提是使用這個類的時候,要先對工程進行編譯。前面用裝備解釋過 Dagger2 這種型別的庫,它會在編譯階段產生中間程式碼,這些中間程式碼就包括自動實現了被 @Component 註解過的介面實現類。

所以,我們如果要使用 Dagger2 為了我們自動生成的類時,我們就應該先 Build->Make Project 編譯一次程式碼。生成的程式碼位置在 app 模組 build 資料夾中,在 AndroidStudio 切換 Project 視角就可以看到。
這裡寫圖片描述

這個目錄下都是 Dagger2 產生的中間產物,DaggerPlatform 就是 Dagger2 為我們自動實現的 Platform 這個介面的實現類,注意它的名字都是 Dagger+介面名稱

有了 DaggerPlatform,我們就能夠使用 Dagger2 進行程式碼的依賴注入了。

public class MainActivity extends AppCompatActivity {

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

        final ZhaiNan zainan = DaggerPlatform.builder()
                .build()
                .waimai();

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(MainActivity.this,zainan.eat(),Toast.LENGTH_LONG).show();
            }
        });

    }
}

然後,測試效果是:
這裡寫圖片描述

需要注意的地方是,Component 的實現類是由 Dagger2 自動生成的,它的名字前面說了是 Dagger+介面名稱。但這是通常情況,因為 @Component 註解的都是頂級類。但還有一種情況是。

class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}

它只是一個內部類的介面,Dagger2 針對這種情況需要把外部的類的名字加下劃線的形式拼接起來,所以上例中 Dagger2 生成的 Component 實現類類名是 DaggerFoo_Bar_BazComponent。

我們並沒有在任何地方用 new 關鍵字親自建立 ZhaiNan 這個類的例項,但是它確實有效,而且它的內部依賴 Baozi 和 Noodle 都被例項化了,也就是說依賴被正確地注入到了 ZhaiNan 的例項物件當中。

所以,@Component 和 @Inject 的配合就能夠使用 Dagger2 了,但這裡面存在一個侷限,@Inject 只能標記在我們自己編寫的類的構造方法中,如果我們使用第三方的庫或者標準庫的話,是不是代表我們對於這些就無能為力了呢?

答案顯然是否定的,Dagger2 作為一款優秀的框架必須考慮到開發過程中的方方面面,不然談何優秀呢?

Dagger2 為了能夠對第三方庫中的類進行依賴注入,提供了 @Provides 和 @Module 兩個註解。

@Provides 和 @Module

Provide 本身的字面意思就是提供,顯然在 Dagger2 中它的作用就是提供依賴。
Module 是模組的意思,Dagger2 中規定,用 @Provides 註解的依賴必須存在一個用 @Module 註解的類中。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle() {
        return new Noodle();
    }
}

public class Baozi {

    String name;

    @Inject
    public Baozi() {
        name = "小籠包";
    }

    public Baozi(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

值得注意的地方有
- @Provides 修飾的方法一般用 provide 作用方法名字首。
- @Module 修飾的類一般用 Module 作為字尾。

前面有講過,@Component 是依賴雙方的聯絡紐帶,現在多了一個 @Module 註解,怎麼配合使用呢?方法,很簡單。只要在 @component 註解後面的括號中取值就是。

@Component(modules = ShangjiaAModule.class)
public interface WaimaiPingTai {
    ZhaiNan waimai();
}

然後編寫測試程式碼

mBtnTestModule = (Button) findViewById(R.id.btn_test_module);

final ZhaiNan zainan1 = DaggerWaimaiPingTai.builder()
        .build()
        .waimai();

mBtnTestModule.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,zainan1.eat(),Toast.LENGTH_LONG).show();
    }
});

演示效果如下:
這裡寫圖片描述

我們再看看 @Provides 用法。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle() {
        return new Noodle();
    }
}

@Provides 註解的方法中直接用 new 建立了依賴,其實還有另外一種方式。我們先對 Noodle 進行重構,讓它作為麵條的基類,然後編寫它一個繼承類。

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

public class Tongyi extends Noodle{

    @Inject
    public Tongyi() {
    }

    @Override
    public String toString() {
        return "統一方便麵";
    }
}

ZhanNan 這個類不用改變,然後,用另外一種方式編寫 @Provides 註解的方法。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle(Tongyi noodle) {
        return noodle;
    }
}

測試程式碼也不需要更改,演示效果如下:
這裡寫圖片描述
那麼,兩種方式有什麼區別呢?

@Provides
public Noodle provideNoodle(Tongyi noodle) {
    return noodle;
}

@Provides
public Noodle provideNoodle(Tongyi noodle) {
    return noodle;
}

什麼時候用 new 關鍵字?什麼時候直接返回傳入進來的引數?
我們不妨再建立一個類 Kangshifu,同樣繼承自 Noodle 這個基類。

public class Kangshifu extends Noodle{

    public Kangshifu() {
    }

    @Override
    public String toString() {
        return "康師傅方便麵";
    }
}

與 Tongyi 這個類不同的地方是,它並沒有用 @Inject 註解構造方法。
我們再嘗試更改 @Provides 註解的相應方法。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle(Kangshifu noodle) {
        return noodle;
    }
//    @Provides
//     public Noodle provideNoodle(Tongyi noodle) {
//        return noodle;
//    }
}

再進行編譯的時候,會發現 IDE 報錯了。

Error:(10, 13) 錯誤: com.frank.dagger2demo.Kangshifu cannot be provided without an @Inject constructor or from an @Provides-annotated method.
com.frank.dagger2demo.Kangshifu is injected at
com.frank.dagger2demo.ShangjiaAModule.provideNoodle(noodle)
com.frank.dagger2demo.Noodle is injected at
com.frank.dagger2demo.ZhaiNan.noodle
com.frank.dagger2demo.ZhaiNan is provided at
com.frank.dagger2demo.WaimaiPingTai.waimai()

Log 提示的錯誤資訊是 Kangshifu 這個類程式碼中沒有被 @Inject 註解過的構造方法,也沒有辦法從一個被 @Provides 註解過的方法中獲取。

所以,什麼時候用 new 建立物件,什麼時候可以直接返回傳入的引數就很明顯了。對於被 @Inject 註解過構造方法或者在一個 Module 中的被 @Provides 註解的方法提供了依賴時,就可以直接返回傳入的引數,而第三方的庫或者 SDK 自帶的類就必須手動建立了。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }


    @Provides
     public Kangshifu provideKangshifu() {
        return new Kangshifu();
    }
//    @Provides
//     public Noodle provideNoodle(Tongyi noodle) {
//        return noodle;
//    }
}

這段程式碼,是可以正常執行的。
這裡寫圖片描述

現在,我有一個新的需求更改。我在 ZhaiNan 類中 eat() 方法中要把餐廳名字打印出來。

public class ZhaiNan {

    @Inject
    Baozi baozi;

    @Inject
    Noodle noodle;

    @Inject
    public ZhaiNan() {

    }

    @Inject
    String resturant;

    public String eat() {
        StringBuilder sb = new StringBuilder();
        sb.append("我從 ");
        sb.append(resturant.toString());
        sb.append("訂的外賣,");
        sb.append("我吃的是 ");
        if ( baozi != null ) {
            sb.append(baozi.toString());
        }

        if (noodle != null) {
            sb.append("  ");
            sb.append(noodle.toString());
        }
        return sb.toString();
    }
}

注意的是,我要新增了一個 String 型別的欄位 resturant 來代表餐廳,並且用 @Inject 註解它。
於是,相應的 Module 也要更改。

@Module
public class ShangjiaAModule {
    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }
    @Provides
     public Noodle provideNoodle(Kangshifu noodle) {
        return noodle;
    }

    @Provides
     public Kangshifu provideKangshifu() {
        return new Kangshifu();
    }

    @Provides
     public String provideResturant() {
        return "王小二包子店";
    }
//    @Provides
//     public Noodle provideNoodle(Tongyi noodle) {
//        return noodle;
//    }
}

然後,再編譯測試。
這次的編譯報錯了。

Error:(10, 13) 錯誤: java.lang.String cannot be provided without an @Inject constructor or from an @Provides- or @Produces-annotated method.
java.lang.String is injected at
com.frank.dagger2demo.ZhaiNan.resturant
com.frank.dagger2demo.ZhaiNan is provided at
com.frank.dagger2demo.Platform.waimai()

報錯的原因是之前編寫的介面 Platform 沒有辦法獲取 ZhanNan 中 resturant 的依賴。因為之前並沒有為 Platform 指定 Module。
那麼現在我們就為它指定 ShangjiaAModule 吧。

編譯後,進行測試,程式正常執行。
這裡寫圖片描述

現在,我把程式碼再重構。在 ShangjiaAModule 中提供餐廳名字的時候,直接返回了“王小二包子店”,這個過於直接,缺少變動,現在針對這個進行變化。

@Module
public class ShangjiaAModule {

    String restaurant;

    public ShangjiaAModule(String restaurant) {
        this.restaurant = restaurant;
    }

    ......

    @Provides
     public String provideResturant() {
        return restaurant;
    }

}

restaurant 在 ShangjiaAModule 建立的時候被賦值,但我們之前的程式碼好像並沒有處理 ShangjiaAModule 的建立,那麼它如何建立呢?
我們先嚐試重新編譯程式碼並執行。

編譯沒有出錯,但執行的時候出錯了。

Caused by: java.lang.IllegalStateException: com.frank.dagger2demo.ShangjiaAModule must be set

at com.frank.dagger2demo.DaggerPlatform$Builder.build(DaggerPlatform.java:64)
at com.frank.dagger2demo.MainActivity.onCreate(MainActivity.java:21)

Log 提示的是呼叫 DaggerPlatform中的Builder.build() 方法時出錯了,因為 ShangjiaAModule 並沒有出錯。我們定位程式碼。

final ZhaiNan zainan = DaggerPlatform.builder()
                .build()
                .waimai();

我之前沒有講的是,如果一個 Module 沒有實現任何構造方法,那麼在 Component 中 Dagger2 會自動建立,如果這個 Module 實現了有參的構造方法,那麼它需要在 Component 構建的時候手動傳遞進去。怎麼傳呢?Component 中生成的 Builder 構造器有與 Module 名字相同的方法,並且引數型別就是 Module 型別。大家細細體會下面程式碼就明白了。

final ZhaiNan zainan = DaggerPlatform.builder()
                .shangjiaAModule(new ShangjiaAModule("王小二包子店"))
                .build()
                .waimai();


final ZhaiNan zainan1 = DaggerWaimaiPingTai.builder()
                .shangjiaAModule(new ShangjiaAModule("衡陽魚粉店"))
                .build()
                .waimai();

再編譯執行。
這裡寫圖片描述

另外,還有一種特殊情況就是,像在 Android 中,MainActivity 這樣的程式碼是我們自己編寫的,所以我們可以給相應的屬性新增 @Inject 註解,但是 MainActivity 物件的建立卻是由 Android Framework 框架決定的,那麼,Dagger2 有沒有針對這種內部擁有 @Inject 標註的屬性,但還沒有進行依賴繫結的類的物件進行依賴注入呢?答案是肯定的。

我們知道,Component 是一個介面,它裡面可以定義很多方法。方法的返回值可以提供一種型別的物件,前提是這個類的物件被 @Inject 註解過構造方法或者在 Module 中被 @Provides 註解過的方法提供。

@Component(modules = ShangjiaAModule.class)
public interface WaimaiPingTai {
    ZhaiNan waimai();
}

ZhaiNan 能夠在一個 Component 中的方法中作為型別返回是因為它符合我上面說的條件,它的構造方法被 @Inject 註解過。

需要注意的是,Component 中方法除了可以返回型別,還可以在方法中傳入型別引數。目的是針對這個引數物件進行依賴注入。
比如

@Component(modules = ShangjiaAModule.class)
public interface WaimaiPingTai {
    ZhaiNan waimai();

    void zhuru(ZhaiNan zhaiNan);
}

我新增了一個方法,zhuru() 中的引數就是 ZhaiNan 型別,代表 DaggerWaimaiPingTai 呼叫這個方法時能夠對一個 ZhaiNan 物件進行依賴注入。

可以編寫程式碼驗證。

mBtnTestZhuru = (Button) findViewById(R.id.btn_test_zhuru);
final ZhaiNan zhaiNan = new ZhaiNan();
WaimaiPingTai daggerWaimaiPingTai = DaggerWaimaiPingTai.builder()
        .shangjiaAModule(new ShangjiaAModule("常德津市牛肉粉"))
        .build();
// 通過呼叫介面中的方法給 zhaiNan 進行依賴注入
daggerWaimaiPingTai.zhuru(zhaiNan);

mBtnTestZhuru.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,zhaiNan.eat(),Toast.LENGTH_LONG).show();
    }
});

執行結果如下:
這裡寫圖片描述

所以,我們可以給介面方法引數傳值的形式來給 Activity 進行依賴注入。

@Module
public class ActivityModule {

    @Provides
    public int provideActivityTest(){
        return 1234567890;
    }
}


@Component(modules = {ShangjiaAModule.class,ActivityModule.class})
public interface WaimaiPingTai {
    ZhaiNan waimai();

    void zhuru(ZhaiNan zhaiNan);

    void inject(MainActivity mainActivity);
}  

我們編寫了新的 Module,然後把它放時 WaimaiPingTai 這個 Component 中去,再添加了 inject() 方法,為的是能夠給 MainActivity 例項進行依賴注入。

現在我們新增測試程式碼,首先在 MainActivity 中新增一個 int 型別的成員變數。

@Inject
int testvalue;

然後要呼叫相關注入方法

mBtnTestActivity = (Button) findViewById(R.id.btn_test_inject_act);
final ZhaiNan zhaiNan = new ZhaiNan();
WaimaiPingTai daggerWaimaiPingTai = DaggerWaimaiPingTai.builder()
        .shangjiaAModule(new ShangjiaAModule("常德津市牛肉粉"))
        .build();

daggerWaimaiPingTai.inject(this);

mBtnTestActivity.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this,"testvalue is "+ testvalue,Toast.LENGTH_LONG).show();
    }
});

執行結果:
這裡寫圖片描述

Component 的建立方式

我們可以看到,建立 Component 都是通過它的 Builder 這個類來進行構建的。其實還有另外一種方式。那就是直接呼叫 Component 實現類的 create() 方法。

public class Test {}

@Component(modules = TestCreate.class)
public interface TestCreateComponent {
    Test ceshi();
}

TestCreateComponent testCreateComponent = DaggerTestCreateComponent.create();
Test test = testCreateComponent.ceshi();

上面程式碼中建立 TestCreateComponent 並沒有藉助於 Builder,而是直接呼叫了 DaggerTestCreateComponent 的 create() 方法,但是它有一個前提,這個前提就是 Component 中的 module 中被 @Provides 註解的方法都必須是靜態方法,也就是它們必須都被 static 修飾。

@Module
public class TestCreate {

    @Provides
    public static int provideTest1() {
        return 1;
    }

    @Provides
    public static String provideTest2() {
        return "test component create()";
    }

    @Provides
    public static Test provideTest(){
        return new Test();
    }
}

因為不需要建立 Module 物件例項,所以 Builder 自然就可以省去了。

@Inject 和 @Provides 的優先順序

可能有心思細膩的同學會問,同樣是提供依賴,如果一個類被 @Inject 註解了構造方法,又在某個 Module 中的 @Provides 註解的方法中提供了依賴,那麼最終 Dagger2 採用的是哪一個?

public class Baozi {

    String name;

    @Inject
    public Baozi() {
        name = "小籠包";
    }

    public Baozi(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return name;
    }
}

@Module
public class ShangjiaAModule {

    String restaurant;

    public ShangjiaAModule(String restaurant) {
        this.restaurant = restaurant;
    }

    @Provides
     public Baozi provideBaozi() {
        return new Baozi("豆沙包");
    }

}

Baozi 這個類就符合我上面給的情景,一方面它確實擁有被 @Inject 註解過的構造方法,另一方面在 Module 中它又通過 @Provides 提供了依賴。那麼,最終,Dagger2 採取了哪一種呢?

答案是 Module,其實現象我們在之前的測試時已經可以觀察到了,最終螢幕顯示的是豆沙包選項。

Dagger2 依賴查詢的順序是先查詢 Module 內所有的 @Provides 提供的依賴,如果查詢不到再去查詢 @Inject 提供的依賴。

到這裡,我們講解了 Dagger2 中最常見的 4 個註解:@Inject、@Component、@Module、@Provides。

正常情況下,這 4 個註解能夠很好的完成一般的程式碼開發了。但是,這都是基礎功能,Dagger2 提供了更多的一些特性。

Dagger2 中的單例 @Singleton

我們在平常開發中經常要涉及到各種單例。比如在 Android 中開發,資料庫訪問最好要設計一個單例,網路訪問控制最好設計一個單例。我們經常編寫這樣的程式碼。

public class DBManager {

    private static DBManager instance;

    private DBManager() {
    }

    public