Android開發知識:Dagger2入門

胖宅老鼠發表於2019-05-09

1、前言

Dagger2作為一個上手難度較高的框架,我也是看了許多相關的文章,經歷了無數次的從入門到放棄。放棄的多了好像也有一點懂了,於是乎我也總結一下自己對Dagger2使用的相關知識的理解。

2、依賴注入

關於Dagger2首先要理解的就是依賴注入(DI)和控制反轉(IOC),對這兩個概念你如果已經有所瞭解,可以直接跳到下一節。

在理解依賴注入之前先了解依賴注入的目的,也是使用Dagger2框架的目的,知道了目的才能更好地理解過程。依賴注入的目的就是二個字:解耦

高內聚,低耦合,是物件導向程式設計提倡遵循的設計原則,而通過依賴注入的方式能實現控制反轉,從而實現解耦的目的。

光這樣說還是太理論了,不易理解,舉個例子來幫助理解下,最近復聯4大火,舉個漫威英雄的例子。

Android開發知識:Dagger2入門

首先想象一下,你是個自帶柯南屬性普通人,每次有外星人入侵或者是超能力變種人搞破壞,你都好巧不巧的能出現在現場,然而你並沒有能力打敗他們,你只有託尼史塔克的聯絡方式,所以你每次都聯絡他,由他來解決這些麻煩。然而託尼突然有一天告訴你他要帶著小辣椒去度假,會有一段時間聯絡不上。你沒辦法出於求生本能,你得去尋找另一個超級英雄,你找到了美國隊長,隊長一口答應,說沒問題,下次有事聯絡我,我來搞定。於是你獲得了美國隊長的聯絡方式。又過一段時間,隊長也和你說他要和冬兵去度假,然而託尼還度假沒回來。你沒辦法,又只能自己去找寡姐,獲得了他的聯絡方式,下次遇到襲擊就聯絡她。發現了沒有,這裡你每次都依賴與某一個超級英雄,一旦發生變故,你只能自己去找新的英雄獲得更新他的聯絡方式。這樣對某個英雄依賴非常嚴重,現在換一種方法,不具體依賴某個英雄,我直接去神盾局找尼克弗瑞得到他的聯絡方式,以後有事都聯絡他,至於是哪個超級英雄來解決又或者怎麼去聯絡英雄,我都不用知道,交給尼克弗瑞去處理就行。

下面通過程式碼加深理解,先定義三個具體英雄類:

//抽象英雄類
public abstract class Hero {
    public abstract String call();
}
複製程式碼
public class IronMan extends Hero {
    @Override
    public String call() {
        return "賈維斯:已收到您的求救訊息,正在聯絡託尼";
    }
}
複製程式碼
public class CaptainAmerica extends Hero {
    @Override
    public String call() {
        return "美國隊長:我已收到您的求救訊息,正在趕來的路上";
    }
}
複製程式碼
public class BlackWidow extends Hero {
    @Override
    public String call() {
        return "黑寡婦:我已收到您的求救訊息,正在趕來的路上";
    }
}
複製程式碼

最後是自己類:

public class Self {
    private IronMan ironMan;
    public Self() {
        ironMan = new IronMan();
    }
    public void help() {
        String call = ironMan.call();
        Log.d("callMessage", call);
    }
}
複製程式碼

呼叫:

   //首先要有一個我
   Self self = new Self();
   //遇到危險
   self.help();
複製程式碼

執行日誌:

D/callMessage: 賈維斯:已收到您的求救訊息,正在聯絡託尼
複製程式碼

託尼去度假了,於是我們只能聯絡美隊,所以要修改Self類:

public class Self {
//    private IronMan ironMan;
    private CaptainAmerica captainAmerica;

    public Self() {
//        ironMan = new IronMan();
        captainAmerica = new CaptainAmerica();
    }

    public void help() {
//        String call = ironMan.call();
        String call = captainAmerica.call();
        Log.d("callMessage", call);
    }
}
複製程式碼

執行日誌:

D/callMessage: 美國隊長:我已收到您的求救訊息,正在趕來的路上
複製程式碼

美隊也去度假了,再次修改Self類:

public class Self {
//    private IronMan ironMan;
//    private CaptainAmerica captainAmerica;
    private BlackWidow blackWidowa;

    public Self() {
//        ironMan = new IronMan();
//        captainAmerica = new CaptainAmerica();
        blackWidowa = new BlackWidow();
    }

    public void help() {
//        String call = ironMan.call();
//        String call = captainAmerica.call();
        String call = blackWidowa.call();
        Log.d("callMessage", call);
    }
}

複製程式碼

執行日誌:

D/callMessage: 黑寡婦:我已收到您的求救訊息,正在趕來的路上
複製程式碼

看到這裡發現每次變動都要修改Self類,這裡Self一直依賴一個英雄類,英雄更換了要修改,英雄的建構函式變了也要修改Self類,這樣耦合就非常嚴重。現在我們採用通過尼克弗瑞來聯絡英雄:

public class NickFury {
    private Hero hero;
    public Hero call() {
        hero = new CaptainAmerica();
        return hero;
    }
}

複製程式碼

修改Self類:

public class Self {
    private NickFury nickFury;
    public Self() {
        nickFury = new NickFury();
    }
    public void help() {
        Hero hero = nickFury.call();
        String call = hero.call();
        Log.d("callMessage", call);
    }
}
複製程式碼

執行日誌:

 D/callMessage:美國隊長:我已收到您的求救訊息,正在趕來的路上  
複製程式碼

這下Self類不依賴於具體某個英雄類,而是通過三方NickFury類來實現英雄物件的注入。一旦有所變動更換英雄,只需要修改NickFury類的方法即可。其實這裡的NickFury類,類似於工廠模式。

還記的依賴注入的目的嗎?解耦,這裡通過第三方工廠類使具體英雄類與Self類不再耦合,原來是Self主動去new例項化一個英雄類,修改後變為被動通過呼叫第三方類方法注入一個英雄類,由主動到被動實現了控制反轉,實現瞭解耦,達成了這個目的。

關於依賴注入的方法有以下幾種:

  • 基於介面注入
  • 基於建構函式注入
  • 基於 set 方法注入
  • 基於註解注入

基於介面:

public interface InjectInterface {
    void injectHero(Hero hero);
}
複製程式碼
public class MySelf implements InjectInterface {
    Hero hero;
    public void help() {
        String call = hero.call();
        Log.d("callMessage", call);
    }
    @Override
    public void injectHero(Hero hero) {
        this.hero = hero;
    }
}
複製程式碼

定義一個介面,實現類實現介面方法注入。

基於建構函式:

public class MySelf  {
    private Hero hero;
    public MySelf(Hero hero) {
        this.hero = hero;
    }
    public void help() {
        String call = hero.call();
        Log.d("callMessage", call);
    }
}
複製程式碼

在建構函式時傳入。

基於set方法:

public class MySelf  {
    private Hero hero;
    public void setHero(Hero hero) {
        this.hero = hero;
    }
    public void help() {
        String call = hero.call();
        Log.d("callMessage", call);
    }
}
複製程式碼

通過set方法注入物件。

基於註解注入:

Dagger2中就用到了註解。所以這裡用Dagger2來實現一個了依賴注入的例子。再想象一個吃麻辣燙場景,麻辣燙裡要加很多食材,比如牛肉、豆腐、香腸、魚丸等而香腸又是由腸衣和肉餡組成,魚丸是由魚肉做成。所以程式碼一般是這樣寫:

public class TestActivity extends AppCompatActivity {
     Fish fish;
     FishBall fishBall;
     Doufu doufu;
     Potato potato;
     Meat meat;
     Casings casings;
     SeeYouTomrrow seeYouTomrrow;
     Sausage sausage;
     SpicyHotPot spicyHotPot;
     Beef beef;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        
        beef = new Beef();
        fish = new Fish();
        fishBall = new FishBall(fish);
        doufu = new Doufu();
        potato = new Potato();
        meat = new Meat();
        casings = new Casings();
        sausage = new Sausage(casings, meat);
        seeYouTomrrow = new SeeYouTomrrow();
        spicyHotPot = new SpicyHotPot(potato, doufu, sausage, seeYouTomrrow, fishBall,beef);

        spicyHotPot.eat();
    }
}
複製程式碼

這裡就是初始化了很多物件,其中有些物件中還引用了其他物件,像這樣的初始化程式碼在平常開發中還是比較常見的,而使用了Dagger2就可以這樣寫:

public class TestActivity extends AppCompatActivity {
    @Inject
    SpicyHotPot spicyHotPot;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);

        DaggerTestComponent.builder().build().inject(this);

        spicyHotPot.eat();
    }
}
複製程式碼

使用了Dagger2是不是簡單了不少,只要一個註解加一行程式碼,就完成了多個物件的初始化,而且無論物件如何修改,這裡的程式碼都無需變動,完成了解耦。

3、Dagger2註解使用

既然Dagger2是通過註解實現的依賴注入,那麼學習使用Dagger2就是要學習Dagger2中的註解的使用。不過,在具體看Dagger2中的註解之前,先要在專案中引入Dagger2的依賴,按照GithubDagger2的文件引入:

dependencies {
  implementation 'com.google.dagger:dagger:2.22.1'
  annotationProcessor 'com.google.dagger:dagger-compiler:2.22.1'
}
複製程式碼

接下來來看Dagger2具體的註解。

3.1 @Inject和@Component註解

@Inject註解的作用主要有兩個:

  • 一是標註在成員變數上,表示需要通過Dagger2為它提供依賴。
  • 二是標註在建構函式上,表示為這個型別的成員變數提供依賴。

所以我們要使用Dagger2初始化一個類的依賴首先要在這個類的建構函式上加上@Inject註解,然後在需要依賴的地方的對應變數上也加上@Inject註解。但是光加上@Inject註解完成所謂的依賴注入嗎?答案是否定的,@Inject只是標註了依賴的需求方和依賴的提供方,但是它們倆之間還沒有建立關係橋樑。而@Component就是幹這個的,具體來看下面這個例子:

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

    public String returnName() {
        return "百事可樂";
    }
}
複製程式碼

先定義一個可樂類,在他的建構函式上加上@Inject註解,表示提供該類的依賴。再在Activity中定義一個Cola型別變數同樣加上@Inject註解,表示需要該類依賴:

public class InjectAndComponentActivity extends AppCompatActivity {

    private TextView mTextContent;
    @Inject
    Cola cola;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inject_and_component);
        mTextContent = findViewById(R.id.textContent);
    }
}

複製程式碼

接著新建一個介面ColaComponent

@Component
public interface ColaComponent {
    void inject(InjectAndComponentActivity injectAndComponentActivity);
}
複製程式碼

介面上標註了@Component註解,接著點選AndroidStudio上的Make Project編譯專案,此時Dagger2會自動生成這個介面的實現類DaggerColaComponent,由這個實現類的方法來完成依賴注入。接著在Activity中通過實現呼叫他的inject方法完成注入。

public class InjectAndComponentActivity extends AppCompatActivity {

    private TextView mTextContent;
    @Inject
    Cola cola;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_inject_and_component);
        mTextContent = findViewById(R.id.textContent);

        DaggerColaComponent.builder().build().inject(this);

        mTextContent.setText(cola.returnName());
    }
}

複製程式碼

注入之後就可以呼叫該物件的方法,執行效果:

Android開發知識:Dagger2入門

上面舉的麻辣燙的例子的實現也是一樣的,每個食材類的建構函式上加了@Inject註解,建立一個Component介面。貼上部分程式碼:

//腸衣
public class Casings {
    @Inject
    public Casings() {
    }
}
//肉
public class Meat {
    @Inject
    public Meat() {
    }
}
//香腸
public class Sausage {
    Casings casings;
    Meat meat;
    @Inject
    public Sausage(Casings casings, Meat meat) {
        this.casings = casings;
        this.meat = meat;
    }

}
//麻辣燙
public class SpicyHotPot {
    Potato potato;
    Doufu doufu;
    Sausage sausage;
    SeeYouTomorrow seeYouTomorrow;
    FishBall fishBall;
    Beef beef;
    @Inject
    public SpicyHotPot(Potato potato, Doufu doufu, Sausage sausage, SeeYouTomorrow seeYouTomorrow, FishBall fishBall, Beef beef) {
        this.potato = potato;
        this.doufu = doufu;
        this.sausage = sausage;
        this.seeYouTomorrow = seeYouTomorrow;
        this.fishBall = fishBall;
        this.beef = beef;
    }
    public void eat() {
        Log.d("Dagger2", "我開動了");
    }
}
複製程式碼

3.2 @Module和@Provides註解

無論是3.1中Cola類還是之前的各種食材類都是我們自己定義的類,所以可以自己修改,想使用Dagger2只需要在類的建構函式上加上@Inject註解。但是實際開發中會遇到使用三方類庫的情況,這些三方類庫中的類程式碼我們無法修改,沒法在其建構函式上加@Inject註解,那麼是不是沒法使用Dagger2了呢?答案還是否定的,Dagger2中的@Module@Provides註解就是用來處理只種情況。
看下面這個例子,這回不吃麻辣燙了,來吃炸雞,於是定義了一個德克士類。

public class Dicos {
    String friedDrumstick;
    public Dicos() {
        friedDrumstick = "脆皮手槍腿";
    }
    public String returnDicos() {
        return "德克士:" + friedDrumstick;
    }
}
複製程式碼

這回不新增任何註解,就是一個正常的Dicos物件類。接著同樣新建一個DicosComponent介面:

@Component
public interface DicosComponent {
    void inject(ModuleAndProvidesActivity moduleAndProvidesActivity);
}
複製程式碼

接著在Activity中定義Dicos型別變數:

public class ModuleAndProvidesActivity extends AppCompatActivity {
    private TextView mTextContent;
    @Inject
    Dicos dicos;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_module_and_provides);
        mTextContent = findViewById(R.id.textContent);
    }
}
複製程式碼

至此為止都和之前是一樣的,接下來新建一個DicosModule類,因為沒法在三方類的建構函式上加@Inject註解,所以要通過Module類來提供依賴。

@Module
public class DicosModule {

    @Provides
    public Dicos getDicos() {
        return new Dicos();
    }

}
複製程式碼

這裡首先建立了一個DicosModule類,並在DicosModule類上加上@Module註解,接著在這個類中只寫了一個getDicos()方法,呼叫Dicos的構造方法建立物件然後返回。通過在方法上加上@Provides註解,表示由這個方法為Dicos型別提供依賴。最後記得還要在DicosComponent介面註解上加上DicosModule。這樣在Activity@Inject標記需要依賴時,才能找到。

@Component(modules = DicosModule.class)
public interface DicosComponent {
    void inject(ModuleAndProvidesActivity moduleAndProvidesActivity);
}
複製程式碼

Make Project後在Activity中同樣呼叫DaggerDiscosComponentinject方法注入依賴即可。

public class ModuleAndProvidesActivity extends AppCompatActivity {
    private TextView mTextContent;
    @Inject
    Dicos dicos;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_module_and_provides);
        mTextContent = findViewById(R.id.textContent);

        DaggerDicosComponent.builder().dicosModule(new DicosModule()).build().inject(this);
        mTextContent.setText(dicos.returnDicos());
    }
}
複製程式碼

執行結果:

Android開發知識:Dagger2入門

3.3 @Named和@Qualifier註解

接下來考慮一下這種情況,如果一個類有多個構造方法,或者有兩個相同依賴時,它們都繼承同一個父類或者實現同一個介面,那麼怎麼區分呢?這就要用到@Named或者@Qualifier註解了。這次定義一個小肥羊火鍋類。

public class LittleSheep {
    String mutton = "沒有肉";
    String vegetables = "沒有菜";

    public LittleSheep(String mutton) {
        this.mutton = mutton;
    }

    public LittleSheep(String mutton, String vegetables) {
        this.mutton = mutton;
        this.vegetables = vegetables;
    }
    public String retrunCotent() {
        return "小肥羊火鍋:" + mutton + " " + vegetables;
    }
}
複製程式碼

這個類有兩個建構函式,接著同樣是建立ComponentModule類。

@Component(modules = LittleSheepModule.class)
public interface LittleSheepComponent {
    void inject(NamedActivity namedActivity);
}
複製程式碼
@Module
public class LittleSheepModule {
    @Named("all")
    @Provides
    public LittleSheep provideLittleSheepAll() {
        return new LittleSheep("十盤羊肉", "一盤蔬菜");
    }

    @Named("mutton")
    @Provides
    public LittleSheep provideLittleSheepSingle() {
        return new LittleSheep("二十盤羊肉吃個夠");
    }
}
複製程式碼

LittleSheepComponent和之前的沒什麼區別,看到LittleSheepModule,這個Module有兩個@Provides註解方法,分別對應兩個不同傳參的建構函式,但是這兩個方法返回型別都是LittleSheep都提供同一型別的依賴,在需求依賴的時候Dagger2就分不清是哪個了,所以這裡要用@Named註解來做個區分,這裡分別設定兩個不同的name,allmutton做區分。除此之外在Activity中,需求依賴的時候也要使用@Named做區分。

public class NamedActivity extends AppCompatActivity {
    private TextView mTextContent;
    @Named("all")
    @Inject
    LittleSheep allLittleSheep;
    @Named("mutton")
    @Inject
    LittleSheep littleSheep;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_named);
        mTextContent = findViewById(R.id.textContent);

        DaggerLittleSheepComponent.builder().build().inject(this);

        mTextContent.setText(allLittleSheep.retrunCotent() + "\n" + littleSheep.retrunCotent());
    }
}
複製程式碼

NamedActivity中同樣在@Inject註解上還要加上@Named區分。執行結果:

Android開發知識:Dagger2入門

接下來看繼承同一個父類的情況,首先定義一個快餐類FastFood,再定義他的兩個子類KFCBurgerKing

public abstract class FastFood {
    public abstract String returnContent() ;
}
複製程式碼
public class KFC extends FastFood {

    public KFC() {
    }

    @Override
    public String returnContent() {
        return "KFC全家桶";
    }
}
複製程式碼
public class BurgerKing extends FastFood {

    String beefBurger = "三層牛肉漢堡";

    public BurgerKing() {
    }
    @Override
    public String returnContent() {
        return "漢堡王:" + beefBurger;
    }
}
複製程式碼

接著還是建立ComponentModule類。

@Component(modules = FastFoodQualifierModule.class)
public interface FastFoodQualifierComponent {
   void inject(FastFoodQualifierActivity fastFoodQualifierActivity);
}
複製程式碼
@Module
public class FastFoodQualifierModule {

    @Provides
    public FastFood getKFC() {
        return new KFC();
    }

    @Provides
    public FastFood getBurgerKing() {
        return new BurgerKing();
    }
}
複製程式碼

主要的問題還是在Module中,這樣寫還是無法區分到底是KFC還是BurgerKing的依賴。這回不使用@Named使用@Qualifier處理。@Qualifier@Named更加靈活和強大,用於自定義註解。接下來我們定義兩個註解。

@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface KFC {
}
複製程式碼
@Qualifier
@Retention(RetentionPolicy.RUNTIME)
public @interface BurgerKing {
}
複製程式碼

一個KFC和一個BurgerKing註解,存活時長都選擇RUNTIME,再新增上@Qualifier註解限定,接下來就可以用這兩個自定義註解來區分Module中方法的型別。

@Module
public class FastFoodQualifierModule {

    @com.sy.dagger2demo.annotations.KFC
    @Provides
    public FastFood getKFC() {
        return new KFC();
    }
    @com.sy.dagger2demo.annotations.BurgerKing
    @Provides
    public FastFood getBurgerKing() {
        return new BurgerKing();
    }

}
複製程式碼

FastFoodQualifierModule中兩個方法上分別新增剛才的兩個註解,接下來Activity中和使用@Named並沒什麼不同,只是把@Named註解分別換成剛才定義的註解。

public class FastFoodQualifierActivity extends AppCompatActivity {

    private TextView mTextContent;
    @KFC
    @Inject
    FastFood kfc;
    @BurgerKing
    @Inject
    FastFood burgerKing;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fast_food_qualifier);
        mTextContent = (TextView) findViewById(R.id.textContent);

        DaggerFastFoodQualifierComponent.builder().build().inject(this);

        mTextContent.setText(kfc.returnContent()+"\n"+burgerKing.returnContent());
    }
}
複製程式碼

執行結果:

Android開發知識:Dagger2入門

3.4 @Singleton和@Scope註解

@Singleton看名字就知道這個註解是用來實現單例的。再次定義一個NetworkClient類。採用@Module@Provides註解實現依賴注入。

public class NetworkClient {
    String baseUrl;
    public NetworkClient() {
        baseUrl = "http://www.baidu.com/";
    }
    public void init() {
        Log.d("NetworkClient", "網路初始化");
    }
}
複製程式碼
@Component(modules = NetworkModule.class)
public interface NetworkComponent {
   void inject(SingletonAndScopeActivity singletonAndScopeActivity);
}
複製程式碼
@Module
public class NetworkModule {
    @Provides
    public NetworkClient getClient() {
        return new NetworkClient();
    }
}
複製程式碼
public class SingletonAndScopeActivity extends AppCompatActivity {

    private TextView mContentTextView;
    @Inject
    NetworkClient networkClient1;
    @Inject
    NetworkClient networkClient2;

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

        DaggerNetworkComponent.create().inject(this);

        mContentTextView.setText(networkClient1.toString() + "\n" + networkClient2.toString());

    }

}

複製程式碼

這裡的程式碼和之前的完全相同,只是在Activity中新增了一個物件,同時有兩個NetworkClient物件,呼叫hashCode方法檢視是否為同一個物件。 執行結果:

Android開發知識:Dagger2入門

可以看到這裡hashCode不同,明顯是兩個物件。接下來使用@Singleton實現單例。首先在NetworkComponent上加上@Singleton註解:

@Singleton
@Component(modules = NetworkModule.class)
public interface NetworkComponent {
   void inject(SingletonAndScopeActivity singletonAndScopeActivity);
}
複製程式碼

接著在NetworkModule中的方法上也加上@Singleton註解:

@Module
public class NetworkModule {
    @Singleton
    @Provides
    public NetworkClient getClient() {
        return new NetworkClient();
    }
}
複製程式碼

這樣就ok了,就是這麼簡單,再次執行程式檢視hashCode。這時hashCode相同已經是同一個物件了。
執行結果:

Android開發知識:Dagger2入門

注意這裡其實@Singleton實現的單例只是在同個Activity下的單例,在其他Activity下,再次建立這了類的物件就不再是同一個物件了。這裡我們在新建一個Activity測試下:

public class SecondActivity extends AppCompatActivity {

    private TextView mContentTextView;
    @Inject
    NetworkClient networkClient;

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

        DaggerNetworkComponent.create().inject(this);

        mContentTextView.setText(networkClient.toString());

    }

}

複製程式碼

Component中新增inject方法型別為SecondActivity

@Singleton
@Component(modules = NetworkModule.class)
public interface NetworkComponent {
   void inject(SingletonAndScopeActivity singletonAndScopeActivity);
   void inject(SecondActivity secondActivity);
}
複製程式碼

執行結果:

Android開發知識:Dagger2入門

可以看到雖然使用了@Singleton註解,但是也只能保證在同一個Activity中是單例同一個物件,在多個Activity中就無法保證了。那麼怎麼實現全域性的單例呢?這可以用@Scope註解。

@Scope同樣用來自定義註解用來限定註解,因為我們知道Application是單例的,所以可以使用@Scope結合Application實現全域性的單例模式。先定義一個新註解ApplicationScope

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ApplicationScope {
}
複製程式碼

這個和上面類似,只是用了@Scope註解,接著修改之前的NetworkModule,將@Singleton換成新註解@ApplicationScope

@Module
public class NetworkModule {
    @ApplicationScope
    @Provides
    public NetworkClient getClient() {
        return new NetworkClient();
    }
}
複製程式碼

接著建立一個新的ActivityComponent

@ApplicationScope
@Component(modules = NetworkModule.class)
public interface ActivityComponent {
   void inject(SingletonAndScopeActivity singletonAndScopeActivity);
   void inject(SecondActivity secondActivity);
}
複製程式碼

這裡同樣是使用了@ApplicationScope註解,接著建立MyApplicatin類在其中去獲取ActivityComponent的例項。

public class MyApplication extends Application {

    ActivityComponent activityComponent;
    @Override
    public void onCreate() {
        super.onCreate();
        activityComponent = DaggerActivityComponent.builder().build();
    }

    public static MyApplication getApplication(Context context){
        return (MyApplication) context.getApplicationContext();
    }

   public ActivityComponent getActivityComponent(){
        return activityComponent;
    }
}
複製程式碼

這裡看到在MyApplication中通過Dagger2獲取到ActivityComponet的例項再給出public方法獲取這個ActivityComponent例項。這樣在Activity中就可以通過這個ActivityComponent注入,獲得單例的物件了。修改Activity中程式碼:

public class SingletonAndScopeActivity extends AppCompatActivity {
    private TextView mContentTextView;

    @Inject
    NetworkClient networkClient1;
    @Inject
    NetworkClient networkClient2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_singleton_and_scope);
        mContentTextView = (TextView) findViewById(R.id.contentTextView);
//        DaggerNetworkComponent.create().inject(this);
        //通過Application中的Component注入
        MyApplication.getApplication(this).getActivityComponent().inject(this);
        mContentTextView.setText(networkClient1.hashCode() + "\n" + networkClient2.hashCode());

        mContentTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(SingletonAndScopeActivity.this,SecondActivity.class));
            }
        });

    }

}
複製程式碼

public class SecondActivity extends AppCompatActivity {

    private TextView mContentTextView;

    @Inject
    NetworkClient networkClient;

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

//        DaggerNetworkComponent.create().inject(this); 
        //通過Application中的Component注入
        MyApplication.getApplication(this).getActivityComponent().inject(this);

        mContentTextView.setText(networkClient.hashCode()+"");

    }
}
複製程式碼

執行結果:

Android開發知識:Dagger2入門

再次執行檢視結果,發現這是已經全是同一個物件了。

3.5 @Component的dependencies

Component還可以通過dependencies依賴於別的Component。這裡再重新定義一個PizzaHut類:

public class PizzaHut {
    String SuperSupremePizza = "超級至尊披薩";

    public PizzaHut() {
    }

    public String returnContent() {
        return "必勝客超級至尊套餐:" + SuperSupremePizza;
    }
}
複製程式碼
@Module
public class PizzaHutModule {
    @Provides
    public PizzaHut getPizzaHut() {
        return new PizzaHut();
    }
}
複製程式碼
@Component(modules = PizzaHutModule.class)
public interface PizzaHutComponent {
   void inject(SingletonAndScopeActivity singletonAndScopeActivity);
   void inject(SecondActivity secondActivity);
}
複製程式碼

同時還建立了對應的ModuleComponet,還是和之前沒什麼區別。接下來在ActivityComponent中使用dependenciesPizzaHutComponent引入。

@ApplicationScope
@Component(modules = NetworkModule.class,dependencies = PizzaHutComponent.class)
public interface ActivityComponent {
   void inject(SingletonAndScopeActivity singletonAndScopeActivity);
   void inject(SecondActivity secondActivity);
}
複製程式碼

接著到MyApplication中引入PizzaHutComponent

public class MyApplication extends Application {

    ActivityComponent activityComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        activityComponent = DaggerActivityComponent.builder().pizzaHutComponent(DaggerPizzaHutComponent.builder().build()).build();
    }

    public static MyApplication getApplication(Context context) {
        return (MyApplication) context.getApplicationContext();
    }

    public ActivityComponent getActivityComponent() {
        return activityComponent;
    }
}
複製程式碼

最後再到剛才的Activity中新增一個PizzaHut型別變數:

public class SingletonAndScopeActivity extends AppCompatActivity {

    private TextView mContentTextView;

    @Inject
    NetworkClient networkClient1;
    @Inject
    NetworkClient networkClient2;
    @Inject
    PizzaHut  pizzaHut;

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

//        DaggerNetworkComponent.create().inject(this);
        MyApplication.getApplication(this).getActivityComponent().inject(this);
        mContentTextView.setText(networkClient1.hashCode() + "\n" + networkClient2.hashCode()+"\n"+pizzaHut.returnContent());

        mContentTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(SingletonAndScopeActivity.this,SecondActivity.class));
            }
        });

    }

}
複製程式碼

執行結果:

Android開發知識:Dagger2入門

可以看到這裡已經成功注入PizzaHut,並且呼叫了returnContent方法。

3.6 懶載入

Dagger2也支援懶載入模式,就是@Inject的時候不初始化,而到使用的時候呼叫get方法獲取例項。

public class SingletonAndScopeActivity extends AppCompatActivity {

    private TextView mContentTextView;

    @Inject
    NetworkClient networkClient1;
    @Inject
    NetworkClient networkClient2;
    @Inject
    Lazy<PizzaHut> pizzaHut;

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

//        DaggerNetworkComponent.create().inject(this);
        MyApplication.getApplication(this).getActivityComponent().inject(this);
       //使用的時候呼叫get方法獲取處理
        mContentTextView.setText(networkClient1.hashCode() + "\n" + networkClient2.hashCode()+"\n"+pizzaHut.get().returnContent());

        mContentTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(SingletonAndScopeActivity.this,SecondActivity.class));
            }
        });
    }
}
複製程式碼

4、Dagger2與MVP

在對Dagger2中的註解有了一定的瞭解之後,繼續學習Dagger2+MVP。將Dagger2MVP結構結合起來,可以使MVP架構中的依賴更加清晰更加易於管理。學習MVP+Dagger2自然是去看Google官方提供的Demo

先看一下工程目錄:

Android開發知識:Dagger2入門
看到具體模組包下除了基礎MVPPresenterContractFragment等類之外,還有ComponentModule這些類都是使用了Dagger2會用到的。接著先來具體看ToDoApplication類:

public class ToDoApplication extends Application {
    private TasksRepositoryComponent mRepositoryComponent;
    @Override
    public void onCreate() {
        super.onCreate();
        mRepositoryComponent = DaggerTasksRepositoryComponent.builder()
                .applicationModule(new ApplicationModule((getApplicationContext())))
                .tasksRepositoryModule(new TasksRepositoryModule()).build();
    }
    public TasksRepositoryComponent getTasksRepositoryComponent() {
        return mRepositoryComponent;
    }
}
複製程式碼

看到這裡就是構建了一個TasksRepositoryComponent,並提供了一個獲得的方法。先找到TaskRepositoryComponent

@Singleton
@Component(modules = {TasksRepositoryModule.class, ApplicationModule.class})
public interface TasksRepositoryComponent {
    TasksRepository getTasksRepository();
}
複製程式碼

首先看到這裡用了單例的註解,接著看到有兩個Module,還提供了一個獲取TaskRepository的方法,這個TaskRepository是用來獲取資料的,在Presenter構造中傳入,Presenter呼叫其中方法獲得資料。先來看TaskRepositoryModule

@Module
public class TasksRepositoryModule {

    @Singleton
    @Provides
    @Local
    TasksDataSource provideTasksLocalDataSource(Context context) {
        return new TasksLocalDataSource(context);
    }

    @Singleton
    @Provides
    @Remote
    TasksDataSource provideTasksRemoteDataSource() {
        return new FakeTasksRemoteDataSource();
    }
}
複製程式碼

看到這個Module中有兩個@Provides標註的方法,是用來提供測試資料返回的。一個方法是本地資料,一個是模擬遠端資料。返回的都是TasksDataSource型別,所以用了自定義註解@Local@Remote做了區分。點進去看這兩個註解,其定義時都用了@Qualifier註解。

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

@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Remote {
}
複製程式碼

接著返回看ApplicationModule

@Module
public final class ApplicationModule {

    private final Context mContext;

    ApplicationModule(Context context) {
        mContext = context;
    }

    @Provides
    Context provideContext() {
        return mContext;
    }
}
複製程式碼

看到其中只提供了一個上下文的mContext方法。回到ToDoApplication中看到這裡建立ApplicationModule傳入的是ApplicationContext。下面進入tasks這個具體模組頁面檢視Dagger2具體是怎麼和MVP結合的。

public class TasksActivity extends AppCompatActivity {

    private static final String CURRENT_FILTERING_KEY = "CURRENT_FILTERING_KEY";

    private DrawerLayout mDrawerLayout;

    @Inject TasksPresenter mTasksPresenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.tasks_act);

        ......
        
        TasksFragment tasksFragment =
                (TasksFragment) getSupportFragmentManager().findFragmentById(R.id.contentFrame);
        if (tasksFragment == null) {
            // Create the fragment
            tasksFragment = TasksFragment.newInstance();
            ActivityUtils.addFragmentToActivity(
                    getSupportFragmentManager(), tasksFragment, R.id.contentFrame);
        }

        // Create the presenter
        DaggerTasksComponent.builder()
                .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
                .tasksPresenterModule(new TasksPresenterModule(tasksFragment)).build()
                .inject(this);

        ......
    }
    ......
}

複製程式碼

這段是TasksActivity中的程式碼,其中省略掉了一些無關的程式碼。可以看到這和Google官方MVPDemo一樣,是在Activity裡放了一個FragmentFragment作為View使用。看到這裡在TasksPresenter上加了@Inject註解,也就是說這裡是要用Dagger2初始化Presenter。在onCreate方法中通過DaggerTasksComponentinject方法注入TasksPresenter依賴,建立TasksPresenter。接下來來看TasksComponent介面的程式碼:

@FragmentScoped
@Component(dependencies = TasksRepositoryComponent.class, modules = TasksPresenterModule.class)
public interface TasksComponent {
    void inject(TasksActivity activity);
}
複製程式碼

其中除了設定了TasksPresenterModule而且還依賴了TasksRepositoryComponent這個Component。這就讓之前的TasksRepository在這裡也可以使用。接著進入TasksPresenterModule檢視:

@Module
public class TasksPresenterModule {

    private final TasksContract.View mView;

    public TasksPresenterModule(TasksContract.View view) {
        mView = view;
    }

    @Provides
    TasksContract.View provideTasksContractView() {
        return mView;
    }

}
複製程式碼

TasksPresenterModule中標註了mView@Provides方法,為注入View提供了方法。最後看到TasksPresenter

final class TasksPresenter implements TasksContract.Presenter {

    private final TasksRepository mTasksRepository;
    private final TasksContract.View mTasksView;
    private TasksFilterType mCurrentFiltering = TasksFilterType.ALL_TASKS;
    private boolean mFirstLoad = true;

    @Inject
    TasksPresenter(TasksRepository tasksRepository, TasksContract.View tasksView) {
        mTasksRepository = tasksRepository;
        mTasksView = tasksView;
    }

    @Inject
    void setupListeners() {
        mTasksView.setPresenter(this);
    }
    
    ......
    
      }
    }

複製程式碼

TasksPresenter的構造方法上加上@Inject註解提供了依賴。至此所有物件具能由Dagger2提供依賴,在TaskActivity注入依賴,完成了Dagger2MVP的結合,完成了解耦。

  DaggerTasksComponent.builder()
                .tasksRepositoryComponent(((ToDoApplication) getApplication()).getTasksRepositoryComponent())
                .tasksPresenterModule(new TasksPresenterModule(tasksFragment)).build()
                .inject(this);
複製程式碼

5、總結

1. 使用Dagger2的目的是解耦。使用Dagger2更好更清晰地管理專案中類之間的依賴。
2. 關於Dagger2的使用我的理解是:

  • 在大型專案中,Dagger2無疑是解耦利器,特別是在專案由多人合作開發時,無需關注個各類的依賴和構造初始化等實現和變動,全部交給Dagger2處理。
  • 在小型專案中,考慮到類之間的相互依賴關係簡單,並且多為單人開發,開發週期較短等因素,可以不引入Dagger2。
  • 如果專案的業務邏輯複雜,各個類之間的相互依賴複雜,初始化構造複雜,還是應該使用Dagger2。雖然有可能開始不熟練,但是多用用就會了。

相關文章