Android 依賴注入可以更簡單 —— 新版本 Dagger 2 使用教學

Ciel_Yang發表於2017-12-01

今年 3 月 21 號 Dagger 2 在 2.10 版本之後針對 Android 方面做了很大的優化,使用方法也隨之有了不少變化。本次改動除了讓 Dagger 2 的使用更加符合控制反轉原則,還針對 Android 端做出了專門的優化(即所謂 dagger.android) —— 大大簡化了 Android 端 Dagger 2 的使用。現在,不妨隨著本文一起探尋新版本 Dagger 2 的基本使用方法。


閱讀前提

要注意閱讀本文內容前最好對 2.10 版本前的 Dagger 2 比較熟悉,至少要明白它的依賴注入管理機制,最好是實際專案中使用過,不然閱讀時會比較迷茫。當然,一切的 Dagger 2 的說明文章很多,善用搜尋引擎就可以找到不錯的教程,這裡就不再贅述了。
另外,本文的 demo 專案中用到了 Google Samples 專案 Android Architecture 使用的 MVP 實現方式,這個也最好有所瞭解。主要是瞭解它的 todo-mvp-dagger 分支的結構組織方法即可,至少要明白它的功能層次和組織結構。


以前的做法有問題?

想想我們在 2.10 版本之前是怎麼在 Android 端使用 Dagger 2 的?是不是類似下面的程式碼:

public class FrombulationActivity extends Activity {
  @Inject Frombulator frombulator;

  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    // DO THIS FIRST. Otherwise frombulator might be null!
    ((SomeApplicationBaseType) getContext().getApplicationContext())
        .getApplicationComponent()
        .newActivityComponentBuilder()
        .activity(this)
        .build()
        .inject(this);
    // ... now you can write the exciting code
  }
}複製程式碼

程式碼例子取自 Google 官方的 Dagger 2 文件

emmmmm,好像也沒什麼問題啊?

不,其實這段程式碼大有問題!

第一點,程式碼重複度過高:我們要在幾乎每一個需要注入依賴項的 Activity 和 Fragment 裡面來一套這樣的程式碼。這是因為我們在 Android 程式設計中要用到的 Activity、Fragment 等類是繼承自系統的,同時生命週期都由系統管理,所以使用 Dagger 2 的時候就不得不進行手動的處理,也就只有在純粹自己寫的 Java 類中使用 Dagger 2 才感覺更舒服一些。

第二點,違反了控制反轉原則:原有的用法使得被注入的目標類必須要了解注入管理工具的詳細資訊,才能讓注入工作順利進行。即使可以通過介面使得實際程式碼中不必書寫太多的實際類名,但這仍然造成了嚴重的目標類和管理類的緊耦合。

其實,我們以前在使用 Dagger 2 的時候為了解決重複問題也是使用 Live Template 或者設計一個通用的工具函式;而為了保證注入目標類正常工作和注入管理正常進行,就必須在設計業務程式碼的時候並行設計注入管理程式碼。
幸好,2.10 版本針對 Android 系統做出了很大的優化,甚至單獨做了 Android 方面的依賴注入管理庫,提供了針對 Android 的註解和輔助類大大減少了要寫的程式碼。

下面我就用一個簡單的 demo 講解一下新版本的 Dagger 2 的基本使用方法,專案地址 請戳這裡。注意專案有兩個分支,master 分支用來演示基本使用方法,simplify 分支演示簡化用法。


基本使用方法

實際上,新版本的 Dagger 2 可以有多種使用方式,需要使用者構建不同數量的介面、抽象類,但我們先不將最簡化的使用方式,因為用基本但複雜的方法更好理解 Dagger 2 的使用邏輯和結構。
要使用基本的功能,只需要在 app 的 build.gradle 檔案中加入下列依賴程式碼:

implementation 'com.google.dagger:dagger:2.13'
implementation 'com.google.dagger:dagger-android-support:2.13'
annotationProcessor 'com.google.dagger:dagger-compiler:2.13'複製程式碼

Application 注入管理

Application 的注入基本沒有什麼變化,更多的是 Dagger 2 官方建議最好使用 Builder 模式構建 Component,方便靈活的向 Component 中新增構建屬性,比如:

@Singleton
@Component(modules = AppModule.class)
public interface AppComponent {

    void inject(MyApplication application);

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(Application application);

        AppComponent build();
    }
}複製程式碼

Module 和舊版相比沒什麼太大變化:

@Module
public class AppModule {

    @Singleton
    @Provides
    Context provideContext(Application application) {
        return application;
    }
}複製程式碼

而一般在呼叫資料庫或存取 SharedPreferences 檔案時,常常用到 Context,一般會提供 Application 而非 Activity 等。

Activity 注入管理

第一,是方便管理多級 Component,要求在最頂層的即 Application 的 Component 中引入 AndroidInjectionModuleAndroidSupportInjectionModule,程式碼形式如下:

@Singleton
@Component(modules = {AppModule.class,              AndroidSupportInjectionModule.class})
public interface AppComponent {
    // ...
}複製程式碼

@BindInstance 註解方便在 Builder 中加入設定項,傳入要求的例項就能設定 Builder 中的對應屬性。

第二,Activity 的 Component(也可以是某個功能模組的 Component)用 @Subcomponent 註解而非 @Component,同時它還要繼承 AndroidInjector<T>(T 一般為對應的 Activity)。當然,如果有對應的 Module 也不要忘記在註解中用 moduls 新增。比如:

@ActivityScoped
@Subcomponent(modules = MainActivityModule.class)
public interface MainActivityComponent extends AndroidInjector<MainActivity> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<MainActivity> {
    }
}複製程式碼

此處 的 Builder 可繼承 AndroidInjector.Builder<T>,必須對應 @Subcomponent.Builder 註解的抽象類,方便生成 Builder 程式碼。

第三,要在第二步中的 Subcomponent 的父 Module 中用註解標記出 Subcomponents 的內容。
可以把 AppModule 當作父 Module

@Module(subcomponents = {MainActivityComponent.class})
public class AppModule {

    @Singleton
    @Provides
    Context provideContext(Application application) {
        return application;
    }
}複製程式碼

也可以另外寫一個 `ActivityBindingModule 當作父 Module

@Module(subcomponents = {MainActivityComponent.class, DummyActivityComponent.class})
public abstract class ActivityBindingModule {
    // ...
}複製程式碼

相對來說,subcomponents 寫在哪裡其實並不重要,ActivityBindingModule 也不止這個上面一個作用,但個人認為寫到這裡更方便管理。

第四,繫結 Subcomponent 的 Builder 到 AndroidInjector.Factory<T>,方便 Dagger 2 藉助正確的 Builder 型別生成對應的 AndroidInjector 程式碼,為方便管理最好寫到一個統一的 Module 中:

@Module(subcomponents = {MainActivityComponent.class, DummyActivityComponent.class})
public abstract class ActivityBindingModule {

    @Binds
    @IntoMap
    @ActivityKey(MainActivity.class)
    abstract AndroidInjector.Factory<? extends Activity> bindMainActivityInjectorFactory(
            MainActivityComponent.Builder builder);
}複製程式碼

第五,把第四不得 Module 資訊加入到 Application 的 Component 註解:

@Singleton
@Component(
        modules = {AppModule.class, ActivityBindingModule.class,
                AndroidSupportInjectionModule.class}
)
public interface AppComponent {

    // ...
}複製程式碼

注意:我這裡沒有采用官方文件的寫法,使用 Activity 的 Module 來管理 subcomponents 屬性,以及繫結 Builder。因為本文描述的方案邏輯上更好理解,也能更集中地管理相似的程式碼,這也是從 Google Samples 演示專案 Android Architecture 學到的方法。

Fragment 注入管理

如果需要對 Fragment 設定單獨的注入管理類,那麼可以參考 Activity 的方式,不同的是父類的 Module 資訊要放到 Activity 的 Component 註解中,所有管理類資訊如下(本例中也用到了簡單的 MVP 結構):

DummyActivity 相關:

@ActivityScoped
@Subcomponent(modules = {DummyActivityModule.class, DummyFragmentBindingModule.class})
public interface DummyActivityComponent extends AndroidInjector<DummyActivity> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<DummyActivity> {
    }
}
/*************************************/
@Module
public class DummyActivityModule {

    @ActivityScoped
    @Provides
    DummyContract.Presenter provideDummyPresenter(DummyPresenter presenter) {
        return presenter;
    }
}複製程式碼

DummyFragment 相關:

@Module(subcomponents = DummyFragmentComponent.class)
public abstract class DummyFragmentBindingModule {

    @Binds
    @IntoMap
    @FragmentKey(DummyFragment.class)
    abstract AndroidInjector.Factory<? extends Fragment> bindDummyFragment(
            DummyFragmentComponent.Builder builder);
}
/*************************************/
@FragmentScoped
@Subcomponent(modules = DummyFragmentModule.class)
public interface DummyFragmentComponent extends AndroidInjector<DummyFragment> {

    @Subcomponent.Builder
    abstract class Builder extends AndroidInjector.Builder<DummyFragment> {
    }
}
/*************************************/
@Module
public class DummyFragmentModule {

    @FragmentScoped
    @Provides
    DummyContract.View provideDummyView(DummyFragment fragment) {
        return fragment;
    }

    // ...
}複製程式碼

本例子中的 MVP 實現方式,參考了 Google Samples 的 Android Architecture 的 todo-mvp-dagger 分支,即把 Activity 僅僅當作 Fragment 的管理容器,Fragment 作為 MVP 中的 View 角色來對待。

完成注入

第一,Applicaiton 要實現介面 HasActivityInjector,用來實現自動管理 Activity 的 Injector,具體實現方法是固定不變的:

public class MyApplication extends Application implements HasActivityInjector {

    @Inject
    DispatchingAndroidInjector<Activity> mInjector;

    @Override
    public void onCreate() {
        super.onCreate();
        DaggerAppComponent.builder().application(this).build().inject(this);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return mInjector;
    }
}複製程式碼

第二,無需管理 Fragment 的 Injector 的 Activity 直接注入自己需要的依賴即可:

public class MainActivity extends BaseActivity {
    // ...
     @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}複製程式碼

第三,需要管理 Fragment 的 Injector 的 Activity 需要實現介面 HasSupportFragmentInjector,方式類似第一步:

public class DummyActivity extends BaseActivity
        implements HasSupportFragmentInjector {
    @Inject DispatchingAndroidInjector<Fragment> mInjector;
    // ...
    @Override
    protected void onCreate(final Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_dummy_layout);
        // ...
    }
    // ...

    @Override
    public AndroidInjector<Fragment> supportFragmentInjector() {
        return mInjector;
    }
}複製程式碼

第四,對 Fragment 注入依賴:

public final class DummyFragment extends BaseFragment {
    // ...
    @Override
    public View onCreateView(
            @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        AndroidSupportInjection.inject(this);
        View view = inflater.inflate(R.layout.fragment_dummy_layout, container, false);
        unbinder = ButterKnife.bind(this, view);
        return view;
    }
    // ...
}複製程式碼

簡化用法

仔細觀察上面的例子,我們應該能發現其實新版本的 Dagger 2 並沒有更改原來的設定:仍然使用 Component 作注入器,使用 Module 作依賴提供者,也仍然使用多層級樹形 Component 來統合管理整個專案的依賴,同時 @Quilifier@Scope 的作用也沒變。僅僅只是把每個 Activity 和 Fragment 的注入程式碼精簡了,無需知道注入的詳細細節了。
但是,僅僅如此的化,你一定會很疑惑:這個改法確實是更符合依賴反轉原則了,可實在也沒節省多少程式碼啊?別急,基本用法一般只是用來了解原理的,實際使用不會這麼幹的!

簡化管理類

首先,我們要考慮到底是什麼程式碼重複最多,最容易使用工具生成呢?當然是 @Subcomponent 標註的程式碼!因為上面的例子中我們可以看出它們的功能相對較為簡單,只是為了構建一個樹形結構方便管理,所以大部分程式設計情景下這部分的程式碼其實沒有什麼額外的功能。而 Dagger 2 也貼心的提供了簡化的方案:
只要 @Subcomponent標註的 Component 類滿足以下條件,就能簡化:

  • 除了對應的 Builder 外沒有其他的方法
  • Builder 類也不會新增其他的方法去自定義構建屬性

在簡化之前,要先在 app 的 build.gradle 檔案中新增下述依賴:

annotationProcessor 'com.google.dagger:dagger-android-processor:2.13'複製程式碼

然後,就能使用 APT 工具自動生成相應的 @Subcomponent 程式碼,比如 Activity 的 Component 就無需再寫,而是在 ActivityBindingModule 中寫入如下的程式碼:

@Module
public abstract class ActivityBindingModule {

    @ActivityScoped
    @ContributesAndroidInjector(modules = MainActivityModule.class)
    abstract MainActivity bindMainActivity();

    @ActivityScoped
    @ContributesAndroidInjector(
            modules = {DummyActivityModule.class, DummyFragmentBindingModule.class}
    )
    abstract DummyActivity bindDummyActivity();
}複製程式碼

而 Fragment 的 @Subcomponent 註解也如法炮製:

@Module
public abstract class DummyFragmentBindingModule {

    @FragmentScoped
    @ContributesAndroidInjector(modules = DummyFragmentModule.class)
    abstract DummyFragment bindDummyFragment();
}複製程式碼

注意DummyFragmentBindingModule 的註解資訊必須加到同 DummyActivityModule 一樣的地方,即本例中的 bindDummyActivity() 的註解中。

這樣,即使 Activity 大量增加也不用寫大量沒什麼變化的 Component 程式碼了。當然 Module 還是需要的,只不過也可以有部分的簡化。

比如 Applicaiton 的 Module 使用 @Binds 把 Application 例項和 Context 進行繫結:

@Module
public abstract class AppModule {

    @Binds
    abstract Context provideContext(Application application);
}複製程式碼

同理,在 MVP 中如果需要提供 Presenter 介面也可以使用這個辦法,比如:

@Module
public abstract class DummyActivityModule {

    @ActivityScoped
    @Binds
    abstract DummyContract.Presenter bindDummyPresenter(DummyPresenter presenter);
}複製程式碼

只不過這個辦法只能把傳入的例項和返回型別進行繫結,其他複雜的依賴提供方法(比如需要利用傳入引數手動例項化,或進行條件判斷)還是不能簡化的。(這不是廢話嗎...)

簡化注入操作

注入操作很明顯可以簡化,畢竟模式完全相同,簡化的方法就是提供了模板父類 DaggerApplicationDaggerAppCompatActivityDaggerFragment,然後讓需要注入操作的類繼承它們即可。
最後,Application 的程式碼就如下所示:

public class MyApplication extends DaggerApplication {

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

而 Activity 和 Fragment 則最好是讓 BaseActivityBaseFragment 去繼承上面提到的兩個模板父類,在一般的程式碼中自只需要用 @Inject 標出需要注入的元素即可,無需任何額外操作。這樣,在編寫 Activity 和 Fragment 的時候無需考慮依賴注入的細節,只要按照正常流程編寫程式碼,然後不斷檢查、測試程式碼,不斷標記出需要注入的元素即可。

最後,再次提醒相關程式碼不可能全部演示出來,可以去 Dagger 2 Android Demo 檢視具體細節,尤其簡化部分的程式碼重複內容較多文章不作贅述,需要的可以自行檢視 simplify 分支。另外,如果專案有 bug 也歡迎直接提出 issue。


參考

相關文章