聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

Tangpj發表於2019-01-19

本文首發於我的個人部落格

點選進入原文連結

前言

Dagger2是現在非常火的一個依賴注入框架,目前由Google維護,在Github上面已經有12K star了。Dagger2的入門門檻其實是比較高的,據瞭解,目前有很多Android工程師對Dagger2還不甚瞭解,沒有用上Dagger2或者是用法有問題,本文的主旨就是讓Android工程師快速掌握Dagger2並且優雅簡潔地使用Dagger2。這裡為大家奉上一份Dagger2 在Android上的最佳實踐教程。

注意: Dagger2框架的上手難度是比一般的框架更難一些的,所以在練習的時候應該儘量減少干擾因素,儘量少引入其它複雜的第三方庫,最佳做法是隻依賴Android基礎庫和Dagger2 For Android需要的庫。

其它技術文章推薦

給你一個全自動的螢幕適配方案(基於SW方案)!—— 解放你和UI的雙手

Gradle自動實現Android元件化模組構建

技術教程Demo地址(本文的Demo也在裡面喲)?

你的支援,是我前進的動力,如果我的專案對您有幫助的話,可以點下star?

依賴注入

什麼是依賴注入?

維基百科上面的介紹是:在軟體工程中,依賴注入是種實現控制反轉用於解決依賴性設計模式。一個依賴關係指的是可被利用的一種物件(即服務提供端) 。依賴注入是將所依賴的傳遞給將使用的從屬物件(即客戶端)。該服務是將會變成客戶端的狀態的一部分。 傳遞服務給客戶端,而非允許客戶端來建立或尋找服務,是本設計模式的基本要求。

簡單來說依賴注入就是將例項物件傳入到另一個物件中去。

依賴注入的實現

維基百科的說法非常抽象,其實在平常編碼中,我們一直都在使用依賴注入。依賴注入主要有以下幾種方式。

  • 建構函式注入
public class Chef{
    Menu menu;
    public Man(Menu menu){
        this.menu = menu;
    }
}
複製程式碼
  • setter方法注入
public class Chef{
    Menu menu;
    public setMenu(Menu menu){
        this.menu = menu;
    }
}
複製程式碼
  • 介面注入
public interface MenuInject{
    void injectMenu(Menu menu);
}

public class Chef implements MenuInject{
    Menu menu;
    
    @Override
    public injectMenu(Menu menu){
        this.menu = menu;
    }
}
複製程式碼
  • 依賴注入框架
public @Inject class Menu{
    ...
}

public class Chef{
    @Inject
    Menu menu;
}
複製程式碼

從上面的例子可以看出,依賴注入其實就是我們天天都在用的東西。

Dagger2實現依賴注入

為什麼要使用Dagger2?

從上面這個簡單的例子來看,為了實現依賴注入,好像沒必要引入第三方的框架。在只有一個人開發,並且業務像上面這麼簡單的時候,確實是沒必要引入Dagger2。但是如果多人同時開發,並且業務非常複雜呢?例如,我們這裡的Menu需要初始化,而選單也要依賴具體的菜式的呢?如果只是一個地方用到的話,還是能接受的。如果專案中有很多地方同時用到呢?如果這個選單要修改呢?有經驗的開發者可能會想到使用單例模式。但是如果專案中有很多型別的結構的話,那麼我們就需要管理非常多的單例,並且單例可能也需要依賴其它物件。在這種情況下如果有變更需求或者是更換維護人員,都會使簡單的改動變得非常繁瑣,並且容易導致各種各樣的奇怪BUG。所以這裡我們就需要引入第三方的依賴注入工具,讓這個工具來幫助我們實現依賴注入。

Dagger2就是我們需要的第三方依賴注入工具。Dagger2較其它依賴注入工具有一個優勢,就是它是採用靜態編譯的方式編譯程式碼的,會在編譯期生成好輔助程式碼,不會影響執行時效能,這一點非常適合用於移動端。

Dagger2的使用方式

Dagger是通過Component來確認需求與依賴物件的,可以說Component是他們之間的紐帶。如果各位用過Dagger2或者瞭解過Dagger2的教程的話,那麼一定知道,Dagger2的使用方式是十分繁瑣的,每個需要用到依賴注入的地方都需要通過編寫DaggerxxxComponent的模版程式碼來實現依賴注入。要寫非常多的模版程式碼,大大增加了系統的複雜度。筆者在使用Dagger 2.17的時候,發現Google對Dagger 2進行了優化,現在使用Dagger實現依賴注入要寫的程式碼其實非常少,並且複雜度已經有了很大程度的降低了。在這裡,筆者就不介紹舊的使用方式了,使用過Dagger2的同學可以對比這兩種方式的差異,沒有使用過的直接學習新的使用方式就可以了。

Dagger2最簡單的使用方式就是下面這種:

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

public class B{
    @Inject A a;
    ...
}
複製程式碼

這種方法是最簡單的,沒什麼難度。但是在實際的專案中我們會遇到各種各樣的複雜情況,例如,A還需要依賴其它的類,並且這個類是第三方類庫中提供的。又或者A實現了C介面,我們在編碼的時候需要使用依賴導致原則來加強我們的程式碼的可維護性等等。這個時候,用上面這種方法是沒辦法實現這些需求的,我們使用Dagger2的主要難點也是因為上面這些原因導致的。

還是用上面的例子來解釋,假設需要做一個餐飲系統,需要把點好的選單發給廚師,讓廚師負責做菜。現在我們來嘗試下用Dagger2來實現這個需求。

首先,我們需要引入Dagger For Android的一些列依賴庫:

 implementation 'com.google.dagger:dagger-android:2.17'
    implementation 'com.google.dagger:dagger-android-support:2.17' // if you use the support libraries
    implementation 'com.google.dagger:dagger:2.17'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.17'
    annotationProcessor 'com.google.dagger:dagger-android-processor:2.17'
複製程式碼

然後我們實現Chef類和Menu類

Cooking介面

public interface Cooking{
    String cook();
}

複製程式碼

Chef


public class Chef implements Cooking{

    Menu menu;

    @Inject
    public Chef(Menu menu){
        this.menu = menu;
    }

    @Override
    public String cook(){
        //key菜名, value是否烹飪
        Map<String,Boolean> menuList = menu.getMenus();
        StringBuilder sb = new StringBuilder();
        for (Map.Entry<String,Boolean> entry : menuList.entrySet()){
            if (entry.getValue()){
                sb.append(entry.getKey()).append(",");
            }
        }

        return sb.toString();
    }
}
複製程式碼

Menu


public class Menu {

    public Map<String,Boolean> menus;

    @Inject
    public Menu( Map<String,Boolean> menus){
         this.menus = menus;
    }
    
    Map<String,Boolean> getMenus(){
        return menus;
    }

}
複製程式碼

現在我們寫一個Activity,作用是在onCreate方法中使用Chef物件實現cooking操作。我們先來看看不使用Dagger2和使用Dagger2的程式碼區別。

MainActivity

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        setContentView(R.layout.activity_main);
        Map<String, Boolean> menus = new LinkedHashMap<>();
        menus.put("酸菜魚", true);
        menus.put("土豆絲", true);
        menus.put("鐵板牛肉", true);
        Menu menu = new Menu(menus);
        Chef chef = new Chef(menu);
        System.out.println(chef.cook());
    }
}
複製程式碼

DaggerMainActivity

public class DaggerMainActivity extends DaggerActivity {
    @Inject
    Chef chef;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Log.d(TAG,chef.cook());
    }
}
複製程式碼

可以看到,在使用Dagger2的時候,使用者的程式碼會變得非常簡潔。但是,Dagger 2還需要一些列的輔助程式碼來實現依賴注入的。如果用過Dagger2就知道要實現依賴注入的話,需要寫十分多模版程式碼。那麼我們可不可以用更簡單的方式使用Dagger2呢?今天筆者就來介紹一下在Android上使用Dagger2的更簡潔的方案。

我們先來看看在DaggerMainActivity上實現依賴注入還需要哪些程式碼。

CookModules

@Module
public class CookModules {

    @Singleton
    @Provides
    public Map<String, Boolean> providerMenus(){
        Map<String, Boolean> menus = new LinkedHashMap<>();
        menus.put("酸菜魚", true);
        menus.put("土豆絲", true);
        menus.put("鐵板牛肉", true);
        return menus;
    }
}
複製程式碼

ActivityModules

@Module
abstract class ActivityModules {

    @ContributesAndroidInjector
    abstract MainActivity contributeMainActivity();
}
複製程式碼

CookAppComponent

@Singleton
@Component(modules = {
        AndroidSupportInjectionModule.class,
        ActivityModules.class,
        CookModules.class})
public interface CookAppComponent extends AndroidInjector<MyApplication> {

    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<MyApplication>{}

}
複製程式碼

MyApplication

public class MyApplication extends DaggerApplication{

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
        return DaggerCookAppComponent.builder().create(this);
    }
}
複製程式碼

Dagger2 For Android 使用要點分析

  1. CookModules CookModule很簡單,它的目的就是通過@Providers註解提供Menu物件需要的資料。因為Menu是需要依賴一個Map物件的,所以我們通過CookModules給它構造一個Map物件,並自動把它注入到Menu例項裡面。
  2. ActivityModules ActivityModules的主要作用就是通過@ContributesAndroidInjector來標記哪個類需要使用依賴注入功能,這裡標記的是ManActivity,所以MainActivity能通過@Inject註解來注入Chef物件。
  3. CookAppComponent CookAppComponent相當於一個注射器,我們前面定義的Modules就是被注射的類,使用@Inject注入物件的地方就是接收者類。
  4. MyApplication MyAppliction的特點是繼承了DaggerAppliction類,並且在applicationInjector方法中構建了一個DaggerCookAppComponent注射器。

這就是Dagger 2在Android中的使用方案了,在這裡我們可以看到,接收這類(MainActivity)中的程式碼非常簡單,實現依賴注入只使用了:

@Inject
Chef chef;
複製程式碼

在接收類裡面完全沒有多餘的程式碼,如果我們要擴充可以SecondsActivity的話,在SecondsActivity我們要用到Menu類。

那麼我們只需要在ActivityModules中增加:

@ContributesAndroidInjector
abstract SecondsActivity contributeSecondsActivity();
複製程式碼

然後在SecondsActivity注入Menu:

@Inject
Menu menu;
複製程式碼

可以看到,對於整個工程來說,實現使用Dagger2 For Android實現依賴注入要寫的模版程式碼其實非常少,非常簡潔。只需要進行一次配置就可以,不需要頻繁寫一堆模版程式碼。總的來說,Dagger2造成模版程式碼增加這個問題已經解決了。

Demo地址:目錄下的Dagger2Simple就是Demo地址,上面的例子為Dagger2Simple中的simple Modules

Dagger2的優勢

在這裡我們總結下使用Dagger2帶來的優點。

  1. 減少程式碼量,提高工作效率 例如上面的例子中,我們構建一個Chef物件的話,不使用Dagger2的情況下,需要在初始化Chef物件之前進行一堆前置物件(Menu、Map)的初始化,並且需要手工注入到對應的例項中。你想像下,如果我們再加一個Restaurant( 餐館 )物件,並且需要把Chef注入到Restaurant中的話,那麼初始化Restaurant物件時,需要的前置步驟就更繁瑣了。 可能有人會覺得,這也沒什麼啊,我不介意手工初始化。但是如果你的系統中有N處需要初始化Restaurant物件的地方呢?使用Dagger2 的話,只需要用註解注入就可以了。
  2. 自動處理依賴關係 使用Dagger2的時候,我們不需要指定物件的依賴關係,Dagger2會自動幫我們處理依賴關係(例如Chef需要依賴Menu,Menu需要依賴Map,Dagger自動處理了這個依賴關係)。
  3. 採用靜態編譯,不影響執行效率 因為Dagger2是在編譯期處理依賴注入的,所以不會影響執行效率在一定的程度上還能提高系統的執行效率(例如採用Dagger2實現單例,不用加鎖效率更高)。
  4. 提高多人程式設計效率 在多人協作的時候,一個人用Dagger2邊寫完程式碼後,其它所有組員都能通過@Inject註解直接注入常用的物件。加快程式設計效率,並且能大大增加程式碼的複用性。

上面我們介紹完了Dagger2 For Android的基本用法了。可能有些讀者意猶未盡,覺得這個例子太簡單了。那麼我們來嘗試下構建一個更加複雜的系統,深度體驗下Dagger2 For Android的優勢。現在我們在上面這個例子的基礎上擴充下,嘗試開發一個簡單的點餐Demo來深度體驗下。

Dagger2應用實戰

現在我們來看下如何使用Dagger2來開發一個簡單的Demo,這裡筆者開發的Demo是一個簡單的點餐Demo。這個Demo的功能非常簡單,提供了選單展示、選單新增/編輯/刪除和下單功能。而下單功能只是簡單地把菜品名用Snackbar顯示到螢幕上。

Demo展

操作展示

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

Demo地址:目錄下的Dagger2Simple就是Demo地址,上面的例子為Dagger2Simple中的order Modules

程式碼目錄

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

這個Demo採用經典的MVP架構,我們先來簡單分析下Demo的細節實現。

  1. 使用SharedPreferences提供簡單的快取功能(儲存選單)。
  2. 使用Gson把列表序列化成Json格式資料,然後以String的形式儲存在SharedPreferences中。
  3. 使用Dagger2實現依賴注入功能。

這樣基本就實現了一個簡單的點菜Demo了。

Dagger在Demo中的應用解釋

當我們使用SharedPreferences和Gson實現快取功能的時候我們會發現,專案中很多地方都會需要這個SharedPreferences和Gson物件。所以我們可以得出兩個結論:

  1. 專案中多個模組會用到一些公共例項。
  2. 這些公共例項應該是單例物件。

我們看看是如何通過使用Dagger2提供全域性的Modules來實現這型別物件的依賴注入。

CookAppModules

@Module
public abstract class CookAppModules {

    public static final String KEY_MENU = "menu";
    private static final String SP_COOK = "cook";

    @Singleton
    @Provides
    public static Set<Dish> providerMenus(SharedPreferences sp, Gson gson){
        Set<Dish> menus;
        String menuJson = sp.getString(KEY_MENU, null);
        if (menuJson == null){
            return new LinkedHashSet<>();
        }
        menus = gson.fromJson(menuJson, new TypeToken<Set<Dish>>(){}.getType());
        return menus;
    }

    @Singleton
    @Provides
    public static SharedPreferences providerSharedPreferences(Context context){
        return context.getSharedPreferences(SP_COOK, Context.MODE_PRIVATE);
    }

    @Singleton
    @Provides
    public static Gson providerGson(){
        return new Gson();
    }

    @Singleton
    @Binds
    public abstract Context context(OrderApp application);

}
複製程式碼

在這裡以dishes模組為例子,dishes中DishesPresenter是負責資料的處理的,所以我們會在DishesPresenter注入這些例項。

DishesPresenter

public class DishesPresenter implements DishesContract.Presenter{

   private DishesContract.View mView;

   @Inject
   Set<Dish> dishes;

   @Inject
   Gson gson;

   @Inject
   SharedPreferences sp;

   @Inject
   public DishesPresenter(){

   }

   @Override
   public void loadDishes() {
       mView.showDishes(new ArrayList<>(dishes));
   }

   @Override
   public String order(Map<Dish, Boolean> selectMap) {
       if (selectMap == null || selectMap.size() == 0) return "";
       StringBuilder sb = new StringBuilder();

       for (Dish dish : dishes){
           if (selectMap.get(dish)){
               sb.append(dish.getName()).append("、");
           }
       }
       if (TextUtils.isEmpty(sb.toString())) return "";

       return "烹飪: " + sb.toString();
   }

   @Override
   public boolean deleteDish(String id) {
       for (Dish dish : dishes){
           if (dish.getId().equals(id)){
               dishes.remove(dish);
               sp.edit().putString(CookAppModules.KEY_MENU, gson.toJson(dishes)).apply();
               return true;
           }
       }
       return false;
   }


   @Override
   public void takeView(DishesContract.View view) {
       mView = view;
       loadDishes();
   }

   @Override
   public void dropView() {
       mView = null;
   }
}

複製程式碼

上面的程式碼能很好地體驗Dagger2的好處,假如我們專案中有比較複雜的物件在很多地方都會用到的話,我們可以通過這種方式來簡化我們的程式碼。

Dishes模組的UI是由Activity加Fragment實現的,Fragment實現了主要的功能,而Activity只是簡單作為Fragment的外層。它們分別是:DishesActivity和DishesFragment

DishesActivity依賴了DishesFragment物件,而在DishesFragment則依賴了DishesAdapter、RecyclerView.LayoutManager、DishesContract.Presenter物件。

我們先來分別看看DishesActivity與DishesFragment的關鍵程式碼。

DishesActivity

public class DishesActivity extends DaggerAppCompatActivity {

    @Inject
    DishesFragment mDishesFragment;
    
    ...
}
複製程式碼

DishesFragment

public class DishesFragment extends DaggerFragment implements DishesContract.View{

    RecyclerView rvDishes;

    @Inject
    DishesAdapter dishesAdapter;

    @Inject
    RecyclerView.LayoutManager layoutManager;

    @Inject
    DishesContract.Presenter mPresenter;
    
    @Inject
    public DishesFragment(){

    }
    
 }
複製程式碼

DishesFragment通過Dagger2注入了DishesAdapter、RecyclerView.LayoutManager、DishesContract.Presenter,而這些例項是由DishesModules提供的。

DishesModules


@Module
public abstract class DishesModules {

    @ContributesAndroidInjector
    abstract public DishesFragment dishesFragment();

    @Provides
    static DishesAdapter providerDishesAdapter(){
        return new DishesAdapter();
    }
    
    @Binds
    abstract DishesContract.View dishesView(DishesFragment dishesFragment);

    @Binds
    abstract RecyclerView.LayoutManager layoutManager(LinearLayoutManager linearLayoutManager);


}
複製程式碼

這裡我們先說明下這幾個註解的作用。

  • @ContributesAndroidInjector 你可以把它看成Dagger2是否要自動把需要的用到的Modules注入到DishesFragment中。這個註解是Dagger2 For Android簡化程式碼的關鍵,下面的小節會通過一個具體例子來說明。

  • @Module 被這個註解標記的類可以看作為依賴物件的提供者,可以通過這個被標記的類結合其它註解來實現依賴關係的關聯。

  • @Provides 主要作用就是用來提供一些第三方類庫的物件或提供一些構建非常複雜的物件在Dagger2中類似工廠類的一個角色。

  • @Binds 主要作用就是確定介面與具體的具體實現類,這樣說得比較抽象,我們還是看看例子吧。 在DishesFragment中有這麼一句程式碼:

    @Inject
    DishesContract.Presenter mPresenter;
    複製程式碼

    我們知道DishesContract.Presenter是一個介面而這個介面可能有很多不同的實現類,而@Binds的作用就是用來確定這個具體實現類的。以看看PresenterModules的程式碼:

    @Module
    public abstract class PresenterModules {
        @Binds
        abstract DishesContract.Presenter dishesPresenter(DishesPresenter presenter);
    
        ...
    }
    
    複製程式碼

    從這句程式碼可以看出,使用@Inject注入的DishesContract.Presenter物件的具體實現類是DishesPresenter。

Dagger2 For Android是如何注入依賴的?

我們在用Dagger2的時候是通過一些模版程式碼來實現依賴注入的( DaggerXXXComponent.builder().inject(xxx) 這種模版程式碼),但是在Demo中的DishesFragment根本沒看到類似的程式碼啊,那麼這些物件是什麼時候注入到DishesFragment重的呢?

答案就是**@ContributesAndroidInjector**註解

我們先來看看Dagger2是通過什麼方式來實現自動把依賴注入到DishesActivity中的。

ActivityModules

@Module
public abstract class ActivityModules {

    @ContributesAndroidInjector(modules = DishesModules.class)
    abstract public DishesActivity contributesDishActivity();

    @ContributesAndroidInjector(modules = AddEditModules.class)
    abstract public AddEditDishActivity contributesAddEditDishActivity();

}
複製程式碼

沒錯,就是@ContributesAndroidInjector這個註解,modules就代表這個DishesActivity需要依賴哪個Modules。這篇教程我們不解釋它的具體實現原理,你只需要知道@ContributesAndroidInjector的作用就可以了。

我們以前使用Dagger2的時候,需要些很多Component來輔助我們實現依賴注入,而現在我們整個App中只需要寫一個Component就可以了。@ContributesAndroidInjector註解會幫助我們生成其它需要的Component,並且自動處理Component之間的關係,自動幫我們使用生成的Component來注入依賴。

我們先看看我們現在整個模組中唯一存在的Component是怎麼使用的。

OrderAppComponent

@Singleton
@Component(modules = {
        AndroidSupportInjectionModule.class,
        LayoutManagerModules.class,
        CookAppModules.class,
        PresenterModules.class,
        ActivityModules.class})
public interface OrderAppComponent extends AndroidInjector<OrderApp>{

    @Component.Builder
    abstract class Builder extends AndroidInjector.Builder<OrderApp>{
    }

}
複製程式碼

OrderApp

public class OrderApp extends DaggerApplication {


    @Override
    protected AndroidInjector<? extends DaggerApplication> applicationInjector() {
        return DaggerOrderAppComponent.builder().create(this);
    }
}
複製程式碼

為了加深大家對@ContributesAndroidInjecto註解r的理解,我們稍微修改下DishesModules

@Module
public abstract class DishesModules {

    //@ContributesAndroidInjector
    //abstract public DishesFragment dishesFragment();

    @Provides
    static DishesAdapter providerDishesAdapter(){
        return new DishesAdapter();
    }

    @Binds
    abstract DishesContract.View dishesView(DishesFragment dishesFragment);

    @Binds
    abstract RecyclerView.LayoutManager layoutManager(LinearLayoutManager linearLayoutManager);


}
複製程式碼

DishesActivity


public class DishesActivity extends DaggerAppCompatActivity {

    //@Inject
    DishesFragment mDishesFragment;

    Toolbar toolbar;

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

        DishesFragment dishesFragment
                = (DishesFragment) getSupportFragmentManager().findFragmentById(R.id.content_fragment);

        if (dishesFragment == null){
            mDishesFragment = new DishesFragment();//新增程式碼
            dishesFragment = mDishesFragment;
            ActivityUtils.addFragmentToActivity(getSupportFragmentManager(), dishesFragment, R.id.content_fragment);
        }
        initView();

    }
    ...
}
複製程式碼
//DaggerFragment改為Fragment
public class DishesFragment extends Fragment implements DishesContract.View{
}
複製程式碼

這個時候,我們執行的時候會發現,DishesFragment中的依賴注入失敗了,執行時會丟擲空指標異常,沒注入需要的資料。導致這個原因是因為我們在這裡使用new來建立DishesFragment例項的,為什麼使用new的時候會Dagger2沒有幫我們注入例項呢?

當我們使用@Inject來注入DishesFragment的時候,Dagger2會自動幫我們判斷DishesFragment所依賴的物件(@Inject註解標記),如果能直接注入的物件則直接注入到Fragment中,否則則從DishesModules中尋找是否有需要的物件,有的話則注入到DishesFragment中。而我們使用new來建立DishesFragment時Dagger2無法通過DishesModules來查詢物件,因為我們沒有宣告DishesFragment與DishesModules的聯絡,DishesFragment也沒有自動注入註解的標記( 沒有實現HasSupportFragmentInjector )。所以Dagger2無法判斷它們依賴關係也沒辦法自動幫DishesFragment自動注入依賴。

如果我們堅持要使用new的方式來依賴DishesFragment的話,則可以通過@ContributesAndroidInjecto註解來實現它們之間的關聯。具體實現方式如下:

DishesModules

@Module(includes = PresenterModules.class)
public abstract class DishesModules {

    @ContributesAndroidInjector
    abstract public DishesFragment dishesFragment(); //增加這個抽象方法

    @Provides
    static DishesAdapter providerDishesAdapter(){
        return new DishesAdapter();
    }

    @Binds
    abstract DishesContract.View dishesView(DishesFragment dishesFragment);

    @Binds
    abstract RecyclerView.LayoutManager layoutManager(LinearLayoutManager linearLayoutManager);


}
複製程式碼

DishesFragment繼承於DaggerFragment

public class DishesFragment extends DaggerFragment implements DishesContract.View{
    ...
}
複製程式碼

改成這樣,我們通過new方法來建立DishesFragment的時候也能實現通過註解進行依賴注入了,為什麼會這樣呢?因為@ContributesAndroidInjector的作用時幫我們生成需要的Subcomponent,然後在DaggerFragment通過 DispatchingAndroidInjector 物件來實現依賴注入( 底層原理和我們使用DaggerXXXComponent手動實現依賴注入差不多 )。我們可以看看DishesModules中被@ContributesAndroidInjector註解的方法生成的程式碼。

@Module(subcomponents = DishesModules_DishesFragment.DishesFragmentSubcomponent.class)
public abstract class DishesModules_DishesFragment {
  private DishesModules_DishesFragment() {}

  @Binds
  @IntoMap
  @FragmentKey(DishesFragment.class)
  abstract AndroidInjector.Factory<? extends Fragment> bindAndroidInjectorFactory(
      DishesFragmentSubcomponent.Builder builder);

  @Subcomponent
  public interface DishesFragmentSubcomponent extends AndroidInjector<DishesFragment> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<DishesFragment> {}
  }
}
複製程式碼

可以看出,編生成的程式碼符合我們上面的結論。

Dagger2 For Android使用要點

我們現在來總結下,簡化版的Dagger實現依賴注入的幾個必要條件:

  1. 第三方庫通過Modules的@provides註解來提供依賴
  2. 提供一個全域性唯一的Component,並且Modules中需要新增AndroidSupportInjectionModule類,它的作用時關聯需求與依賴之間的關係
  3. Application需要繼承DaggerApplication類,並且在applicationInjector構建並返回全劇唯一的Component例項
  4. 其它需要使用依賴注入的組建都需要繼承Dagger元件名字類,並且需要在相應的Modules中通過@ContributesAndroidInjector註解標記需要注入依賴的組建。

上面四個步驟就是使用Dagger2實現依賴注入的要點了,總的來說,複雜度比之前的方法簡單了非常多,要寫的模版程式碼也減少了非常多。

一般來說,上面的知識點已經足夠讓我們在專案中正常使用Dagger2了,但是在使用中還會遇到一些其它的問題,Dagger2也提供瞭解決方法。如果希望進一步瞭解的話,可以繼續閱讀下文。

Dagger2擴充

@Scope

Scope字面的意思是作用域,在我們使用Dagger2的時候經常會用到@Singleton這個註解,這個註解的意思的作用是提供單例物件。而我們在使用@Singleton這個註解的時候,會同時@Provides和@Component,為什麼要這樣做呢?因為@Scope的作用範圍其實就是單例的作用範圍,這個範圍主要是通過Component來確定的。

所以@Scope的作用就是以指定Component的範圍為邊界,提供區域性的單例物件。我們可以以上面的例子為例驗證這個論點論點。

我們在DishesActivity中增加一句程式碼,作用時注入DishesPresneter物件。

@Inject
DishesContract.Presenter mPresenter;
複製程式碼

從上面的程式碼中,我們知道DishesFragment中也用同樣的方式來注入過DishesPresneter物件,那麼它們有什麼區別的,我們通過除錯功能來看下。

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

可以看出,DishesActivity和DishesFragment中的DishesPresenter不是同一個例項,它們的記憶體地址是不一樣的。如果我們在PresenterModules的dishesPresenter方法中加上@Singleton

@Singleton
@Binds
abstract DishesContract.Presenter dishesPresenter(DishesPresenter presenter);
複製程式碼

可以預見,DishesActivity和DishesFragment中的DishesPresenter會變成同一個例項,在這個例子中@Singleton的作用是提供全域性的單例( 因為OrderAppComponent這個全域性唯一的Component也被標註成@Singleton )。這種用法比較簡單,這裡不再深入。而比較難理解的就是自定義Scope了,下面我們通過一個例子來加深大家對自定義Scope的理解。

@DishesScoped

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

為了使測試效果更明顯,我們稍微修改下Order這個Demo。

DishesModules

@Module
public abstract class DishesModules {
   ...
    @DishesScoped  // 新增註解
    @Binds
    abstract DishesContract.Presenter dishesPresenter(DishesPresenter presenter);
   ...

}
複製程式碼

ActivityModules

@Module
public abstract class ActivityModules {

    @DishesScoped  // 新增註解
    @ContributesAndroidInjector(modules = DishesModules.class)
    abstract public DishesActivity contributesDishActivity();
}
複製程式碼

然後現在我們來執行Demo,看下DishesActivity和DishesFragment中的DishesContract.Presenter的物件:

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

可以看出,它們是同一個物件,這驗證了我們上面的結論。這裡又個小問題就是,我們之前說@Scope是通過Component來確定作用邊界的,但是上面這個例子中,並沒有對任何Component類使用@Dishes註解啊?那麼這裡是如何確認邊界的呢?

我們可以看看Dagger生成的類ActivityModules_ContributesDishActivity,這個類是根據ActivityModules中的contributesDishActivity方法生成的。

@Module(subcomponents = ActivityModules_ContributesDishActivity.DishesActivitySubcomponent.class)
public abstract class ActivityModules_ContributesDishActivity {
  private ActivityModules_ContributesDishActivity() {}

  @Binds
  @IntoMap
  @ActivityKey(DishesActivity.class)
  abstract AndroidInjector.Factory<? extends Activity> bindAndroidInjectorFactory(
      DishesActivitySubcomponent.Builder builder);

  @Subcomponent(modules = DishesModules.class)
  @DishesScoped   //看這裡
  public interface DishesActivitySubcomponent extends AndroidInjector<DishesActivity> {
    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<DishesActivity> {}
  }
}
複製程式碼

謎底揭曉,當我們為contributesDishActivity新增上@DishesScoped註解後,自動生成的DishesActivitySubcomponent類被@DishesScoped註解了。所以@DishesScoped是通過DishesActivitySubcomponent來確認作用範圍的,這也符合上面的結論。

@Scope的實現原理

@Scope實現單例的原理其實很簡單,我們可以看下加了@DishesScoped後Dagger為我們生成的注入輔助程式碼。在這裡我們只看關鍵方法:

private void initialize(final DishesActivitySubcomponentBuilder builder) {
      this.dishesFragmentSubcomponentBuilderProvider =
          new Provider<DishesModules_DishesFragment.DishesFragmentSubcomponent.Builder>() {
            @Override
            public DishesModules_DishesFragment.DishesFragmentSubcomponent.Builder get() {
              return new DishesFragmentSubcomponentBuilder();
            }
          };
      this.dishesPresenterProvider =
          DishesPresenter_Factory.create(
              DaggerOrderAppComponent.this.providerMenusProvider,
              DaggerOrderAppComponent.this.providerGsonProvider,
              DaggerOrderAppComponent.this.providerSharedPreferencesProvider);
      this.dishesPresenterProvider2 = DoubleCheck.provider((Provider) dishesPresenterProvider);   //這句程式碼是實現單例的關鍵。
    }
複製程式碼

可以看到,我們的dishesPresenterProvider2這個物件的初始化是通過雙鎖校驗的方式來實現單例的,所以這個物件是一個單例物件。而其它沒有使用@Spoce註解的類則沒有使用雙鎖校驗的方式實現初始化,Dagger通過@Scope實現單例的原理其實非常簡單。關於@Spoce的介紹就到這裡了,如果需要深入的話,可以進一步檢視Dagger2生成的輔助程式碼。

@Qualifier和@Named註解

除了作用域的問題之外我們還會經常會遇到一個問題,總所周知,Dagger2是自動判斷依賴關係的,如果我們的程式碼中需要使用同一個類生成兩個或多個不同的物件呢?例如我們的LinearManager,我們現在想用Dagger提供一個橫向的Manager,如果直接寫在專案中是會報錯的,因為Dagger無法判斷需要注入/依賴的物件是哪個。如下面的程式碼:

LayoutManagerModules


@Module
public class LayoutManagerModules {

    @Provides
    public LinearLayoutManager providesLinearLayoutManager(Context context){
        return new LinearLayoutManager(context);
    }
    
    @Provides 
    public LinearLayoutManager providesHorizonalLinearLayoutManager(Context context){
        return new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
    }

}

複製程式碼

這段程式碼肯定是會報錯的,如果我們想實現這個功能的話,這個時候我們就需要用到@Qualifier或者@Named註解了。

我們先用@Named來實現上面這個需求。

LayoutManagerModules

@Module
public class LayoutManagerModules {

    @Named("vertical")
    @Provides
    public LinearLayoutManager providesLinearLayoutManager(Context context){
        return new LinearLayoutManager(context);
    }

    @Named("horizontal")
    @Provides
    public LinearLayoutManager providesHorizonalLinearLayoutManager(Context context){
        return new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false);
    }


}
複製程式碼

DishesModules

public class DishesFragment extends DaggerFragment implements DishesContract.View{

    RecyclerView rvDishes;

    @Inject
    DishesAdapter dishesAdapter;

    @Named("horizontal")
    @Inject
    LinearLayoutManager layoutManager;
}
複製程式碼

在注入的時候,我們通過 @Named("horizontal")就能控制實際是注入哪個LayoutManager了。在定義依賴的時候@Name註解要配合@Providers,而在使用的時候配合@Inject來使用。

@Qualifier

@Qualifier的作用和@Named是一樣的,@Name也被@Qualifier註解。在使用@Named的時候需要加上我們定義的key所以略顯麻煩,我們可以通過自定義@Qualifier註解來解決這個問題。而自定義@Qualifier註解的方式和自定義@Spoce是一樣的,非常簡單,這裡不作深入介紹了。

Dagger2還提供了例如懶載入等功能,使用起來都是比較簡單的,這裡限於篇幅就不作進一步介紹了。有興趣的讀者可以查閱原始碼或者看官方文件來體驗下。

小結

Dagger2 For Android是一款非常適合移動端使用的依賴注入框架。它提供了靜態編譯的方式來實現依賴注入,效能非常好。並且最新版本的Dagger 2.17對Android提供了非常友好的支援,現在使用Dagger2的時候,我們不需要再手寫注入程式碼,這一切Dagger2都幫我們自動實現了。總的來說,Dagger2是非常適合於應用到我們的專案中的。並且Dagger2實現依賴注入的方式非常有趣,能掌握這項技術的話,對我們的提升是非常大的,希望各位讀者在閱讀了本文後能夠去體驗一下。

如果這篇文章對你有幫助的話,可以關注下筆者其它的文章,歡迎大家在我的github上面點star哦。

給你一個全自動的螢幕適配方案(基於SW方案)!—— 解放你和UI的雙手

Gradle自動實現Android元件化模組構建

技術教程Demo地址(本文的Demo也在裡面喲)?

你的支援,是我前進的動力,如果我的專案對您有幫助的話,可以點下star?

微信公眾號:

如果希望第一時間收到我的技術文章更新的同學,可以掃描下面二維碼關注我的個人公眾號:程式碼之外的程式設計師,專注於精通Android技術

或收藏我的個人部落格:TANG BLOG

聽說你還不會用Dagger2?Dagger2 For Android最佳實踐教程

相關文章