iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 9
0
Software Development

Android Architecture系列 第 9

Dependency Injection with Dagger2: Part 1

在新增其他功能之前,我想趁現在app規模還很小的時候先套用相依性注入(Dependency Injection;簡稱DI)來增進程式品質。

我們使用Google的DI library Dagger2,它的效能很好並會在build時就檢查有無設置錯誤,更在2.10版時對Android推出了優化的使用方式。如果是第一次接觸Dagger2的話個人覺得上手會有點難,所以今天重點會放在成功建置Dagger2框架,明天再陸續修改現有程式。

Why Dependency Injection?

假設我們有兩個class分別為User和它的寵物Dog:

class User {

    public User() {
        // 每次我們使用User時,都建立了新的Dog instance
        Dog dog = new Dog();
        ...
    }
}

上例中,每次使用new User()時都會在Constructor內呼叫new Dog(),兩個class成為了一對(coupled),而User須依賴Dog才能順利使用,我們便稱Dog為User的Dependency。

我們無法單獨使用User而不要建立新的Dog,這樣會產生幾個問題:

  • Reusablility:若兩個User養同一隻Dog,我們無法只建立一次Dog就讓兩個User共用。
  • Testing:撰寫test case的時候一般會mock目標以外的class來對目標做單獨測試,當我們在User中使用new來建立別的class,我們就無法mock那些class來單獨測試User。
  • Maintainability:當專案規模擴大時,這些成對的class越來越多將變得難以維護。承襲第2點,我們無法做單獨測試的話,若現在Dog中又新增一個class項圈,當項圈壞掉時Dog就無法建立,導致User也無法使用,這樣連鎖的反應將增加我們擴充功能和偵錯的困難度。

那比較好的設計是怎樣呢?

class User {

    private Dog dog;
    
    public User(Dog dog) {
        // 從外部取得Dog
        this.dog = dog;
        ...
    }
}

將Dog改成parameter由外部建立再傳給User,這樣簡單的修改就可以解決上面的3個問題:

  • Reusablility:外部建立好Dog之後可以傳給所有共用的User。
  • Testing:我們將mock好的Dog傳給User就可以對User單獨測試。
  • Maintainability:當項圈壞掉時,我們在外部建立Dog時就會發現,不會到要使用User時才發現。

這樣的設計稱做控制翻轉(Inversion of Control;IoC),即一個class若需要用到其他的class則應由外部傳入,而非在內部用new實例化,也是DI框架的核心概念。

Dagger2

Dagger2的起手比較難理解一點,我們今天先建置完成並測試可注入(inject)就好,目標是將Retrofit service注入MainActivity中使用。

加入gradle dependencies:

// Dagger core
implementation "com.google.dagger:dagger:2.14.1"
annotationProcessor "com.google.dagger:dagger-compiler:2.14.1"
// Dagger android
implementation "com.google.dagger:dagger-android-support:2.14.1"
annotationProcessor "com.google.dagger:dagger-android-processor:2.14.1"

我們使用Dagger核心及新推出的dagger.android,這樣Dagger在Android架構中更符合IoC原則。若你的app沒有使用support library的話就把com.google.dagger:dagger-android-support改成com.google.dagger:dagger-android

1.建立Module:建立一個class並用@Module註記,表示此Module內的物件可以讓其他class用注入的方式取得。

@Module
class AppModule {

    @Provides
    @Singleton
    GithubService provideGithubService() {
        return new Retrofit.Builder()
                .baseUrl("https://api.github.com/")
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(new LiveDataCallAdapterFactory())
                .build()
                .create(GithubService.class);
    }
}

建立method provideGithubService()並加上@Provides標註,此method的回傳型態為GithubService表示在Module內放了GithubService供其他class取用,而method的內容便是此GithubService如何生成。

@Singleton是Dagger會幫我們處理此物件的Singleton pattern讓我們不用自己實作。

這樣我們就在Module中新增了一個Singleton的GithubService可以取用了,達到跟原本Retrofit Manager一樣的效果。

接著建立另一個Module,在此Module中指定哪些Activity/Fragment要用Inject的方式取得物件,我們以MainActivity為例:

@Module
public abstract class BuildersModule {

    @ContributesAndroidInjector
    abstract MainActivity contributeMainActivity();
}

Module的部分到這邊就完成了。

2.建立Component:建立一個interface加上@Component註記,並將要用到的Module加入其中,如此Dagger便會自動產生程式碼讓我們能inject那些Module中的項目。

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

    @Component.Builder
    interface Builder {
        @BindsInstance
        Builder application(GithubApp application);
        AppComponent build();
    }

    void inject(GithubApp app);
}

AndroidSupportInjectionModule是dagger.android協助我們做inject的Module,如果你不是用support library的話就改成AndroidInjectionModule,另外兩個就是我們剛剛建立的Module。

Component寫好後需「Make Project」,快速鍵CTRL+F9

3.於Application中初始化:Make Project完成後Dagger會產生DaggerAppComponent class,接著就能初始化Dagger:

public class GithubApp extends Application implements HasActivityInjector {

    @Inject
    DispatchingAndroidInjector<Activity> dispatchingAndroidInjector;

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

        DaggerAppComponent.builder()
                .application(this)
                .build()
                .inject(this);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
        return dispatchingAndroidInjector;
    }
}

這樣建置就完成啦,接著在Activity中注入GithubService測試看看有無成功。

4.在Activity注入物件:使用@Inject注入物件。

public class MainActivity extends AppCompatActivity {

    @Inject
    GithubService githubService;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        AndroidInjection.inject(this);
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
        if (githubService != null) {
            Timber.d("Hello Dagger!");
        }
        ...
    }
}

我們用@Inject標註GithubService並在onCreate時呼叫AndroidInjection.inject(this),這樣就完成注入了,當MainActivity開啟時就會看到Log印出Hello Dagger表示githubService是可用的。注意AndroidInjection.inject(this)需在super.onCreate之前呼叫

這樣就完成在Module中加入物件並注入到Activity使用了。

回顧一下我們做的內容,首先建立AppModule並在裡面放了GithubService讓其他class取用,接著建立BuilderModule標出是哪些Activity要用Inject來取得物件,最後要建立Component來連結這些Module並在Application中初始化。這樣就可以開始將Module中的物件注入到Activity了。


今天目標是成功建置Dagger所以只在Activity使用,明天會開始在Fragment及自訂Class中使用DI,改進我們目前程式的設計。

GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day09-implement-dagger2

Reference:
Demystifying the new Dagger Android Injection API
Introduction to Dagger 2, Using Dependency Injection in Android
Dagger 2 for Android Beginners


上一篇
LiveData整合API資料與連線狀態
下一篇
Dependency Injection with Dagger2: Part 2
系列文
Android Architecture30

尚未有邦友留言

立即登入留言