標籤雲

搜尋此網誌

2016/02/19

使用 Dagger 2 解決 Dependency Injection

Dagger 2 勝過大多數其他依賴性注入框架的一個主要優點是它嚴格地生成實作(no reflection)
而這意味著它可以被使用在 Android application 中
但還是有一些該注意的事項

Dagger 依賴 ProGuard 去事後處理編譯好的 bytecode
這允許 Dagger 發布在 Android 跟 server 會使用不同的工具鏈去產生在這兩種環境都能有效率執行的 bytecode
此外,Dagger 有一個明確的目標是確保它生成的 java 原始碼始終都可以兼容 ProGuard 的優化

Dagger 的基本精神在於:
Multiple injection points: dependencies, being injected.
Multiple bindings: dependencies, being provided.
Multiple modules: a collection of bindings that implement a feature.
Multiple object graphs: a collection of modules that implement a scope.

而 Dagger 2 與前代的差別在於:
No reflection at all: graph validation, configurations and preconditions at compile time.
Easy debugging and fully traceable: entirely concrete call stack for provision and creation.
More performance: according to google they gained 13% of processor performance.
Code obfuscation: it uses method dispatch, like hand written code.

* Android Studio build.gradle

要在 android 使用 dagger2 必須先編輯專案跟 app 的 build.gradle 檔設定
否則 dagger2 無法正常運作

專案的 build.gradle
buildscript {
    //...

    dependencies {
        classpath 'com.android.tools.build:gradle:1.5.0'
        // 增加 apt 支援
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

app 的 build.gradle
apply plugin: 'com.android.application'
//apply plugin for apt
apply plugin: 'com.neenbedankt.android-apt'

android {
    //...
}

dependencies {
    //...

    //加入 dagger, dagger-compiler, 與 annotation 的 lib dependency
    apt 'com.google.dagger:dagger-compiler:2.1'
    compile 'com.google.dagger:dagger:2.1'
    provided 'javax.annotation:jsr250-api:1.0'
}


* Declaring Dependencies

使用 javax.inject.Inject annotation (Dependency Injection for Java (JSR 330))

@Inject
基本上加了這個標註就表示我們需要依賴性
換句話說,我們用它來告訴 Dagger 這個標註的類別或屬性要是依賴性注入的一部分
因此 Dagger 會建構該類別的實體並滿足它的依賴性

class Thermosiphon implements Pump {
  private final Heater heater;

  @Inject
  Thermosiphon(Heater heater) {
    this.heater = heater;
  }
  ...
}
標註 @Inject 的建構子,需要實體時 Dagger 會取得必要參數並呼叫該建構子

標註 @Inject 的屬性
class CoffeeMaker {
  @Inject Heater heater;
  @Inject Pump pump;
  ...
}
如果 class 有標註 @Inject 的屬性,但沒有標註 @Inject 的建構子
若有需要 Dagger 會注入這些屬性,但並不會建立新的實體
所以加入一個標註的 @Inject 無參數的建構子,可以讓 Dagger 去建立實體

Dagger 還支援 method injection,但比較推薦用建構子或屬性的 injection
Dagger 無法建構裡面沒有任何 @Inject 標註的 class

* Satisfying Dependencies

@Provides 是定義在 modules 裡面的 method
它告訴 Dagger 我們想要如何建構並提供那些被提出的依賴性

預設情況下 Dagger 會滿足建立實體所需要的每一個依賴性
但 @Inject 在以下狀況會無效:
- 不能被建構的 interface
- 無法加上標註的第三方類別
- Configurable objects must be configured!

所以在 @Inject 有所不足或立場尷尬的時候
可以用 @Provides 標註的 method 來滿足依賴性
這個 method 的回傳型別定義了它能滿足哪個依賴性
所有的 @Provides method 都必須屬於 @Module 之下
(為了便於辨識, @Provides methods 命名都以 provide 前綴,而 module classes 則以 Module 後綴)

@Provides static Heater provideHeater() {
  return new ElectricHeater();
}
@Provides method 也可能有自己的依賴性,例如下例如果需要 pump 時會回傳一個 Thermosiphon
@Provides static Pump providePump(Thermosiphon pump) {
  return pump;
}

Module 代表的是有提供依賴性 method 的類別
所以我們定義一個類別並加上 @Module 標註,Dagger 會知道建構類別實體時要去那裡找到需要被滿足的依賴性
module 的一個重要功能是他們被設計為可以分開或組合在一起(multiple composed modules)
@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater() {
    return new ElectricHeater();
  }

  @Provides static Pump providePump(Thermosiphon pump) {
    return pump;
  }
}

* Building the Graph

@Inject 與 @Provides 標註的類別透過它們的依賴性組成一個 graph(物件圖)
Android 的 Application 則是透過一組完整定義的根源(root)來存取這個 graph
在 Dagger 2,這個根是由沒有參數、而且會回傳需要型別的一些 method 組成的一個 interface 定義而成

藉由 @Component 標註以及傳入 modules 參數,Dagger 2 可以自動生成它的實作
而這個實作會與 interface 同名但使用 Dagger 前綴
對該實作呼叫 builder() 方法可以取得一個 builder 用來設定依賴性,之後再呼叫 build() 建立實體
(若該 module 有可存取的預設建構子,可以省略 build() 呼叫,因為 builder 可以直接建立實體)
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
  CoffeeMaker maker();
}
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
    .dripCoffeeModule(new DripCoffeeModule())
    .build();
@Component 基本上是個注入者的角色,它可以找到你所需要的型別實體
或者說它是 @Inject 跟 @Module 之間的橋樑,因為它的主要責任就是把這兩樣連起來
我們可以把一個 interface 加上 @Component 標註,然後列出所有組成這個 component 的全部 @Module
如果缺少了任何 Module,編譯時就會出現錯誤
所有 component 都可以透過 module 知道它們提供依賴性的 scope


注意:如果你的 @Component 不是最上層的類別,那麼生成的 component 將會包含它的封閉類別的名稱並用底線相連

如下例會生成 DaggerFoo_Bar_BazComponent
class Foo {
  static class Bar {
    @Component
    interface BazComponent {}
  }
}
如果 module 的 @Provides methods 全都是 static 的,那它的實作不需要建立實體
而若該 interface 內所有依賴性都不須使用者建立依賴的實體就可以建構,那生成的實作會有 create 方法可用,而不用透過 builder
CoffeeShop coffeeShop = DaggerCoffeeShop.create();

現在我們的 CoffeeApp 可以簡易的使用 Dagger 生成的 CoffeeShop 實作(DaggerCoffeeShop)
來得到完整注入的 CoffeeMaker
public class CoffeeApp {
  public static void main(String[] args) {
    CoffeeShop coffeeShop = DaggerCoffeeShop.create();
    coffeeShop.maker().brew();
  }
}
前面例子說明了如何用較典型的 binding 建構一個 component
但還有一些機制可以促進物件圖的綁定
下面這些就可作為依賴性,並可以用來生成格式完整的 component
- 有用 @Component.modules 或 @Module.includes 直接參照的 @Module 並標註為 @Provides 的 method
- 任何有 @Inject 建構子並 unscoped 或有 @Scope 標註與其匹配的 component
- 具有 component 依賴性的 component 規定的 methods
- component 本身
- 包含任何 subcomponent 的不合規定 builders
- 上述任何 bindings 的 Provider 或 Lazy wrappers
- 任何型別的 MembersInjector

@Scope 是非常強大而有用的功能,所有物件都沒有必要知道如何去管理它們的實體
Dagger 2 有更具體的方式來透過自訂標註去做 scoping
一個 scope 的例子:
有一個標註 @PerActivity 的 class
這個物件只要我們的 activity 還活著它就活著
換句話來說,我們可以定義 scope 的大小(例如 @PerFragment, @PerUser...之類的)

* Singletons and Scoped Bindings

對於標註 @Singleton 的 @Provides method 或可被注入的類別
graph 會對所有 client 使用單一實體
@Provides @Singleton static Heater provideHeater() {
  return new ElectricHeater();
}
標註 @Singleton 的可被注入類別

可以用來提醒潛在維護者,該類別有可能被多執行序共享
@Singleton
class CoffeeMaker {
  ...
}

由於 Dagger 2 會把物件圖 scope 的實體與 component 實作實體互相關聯

因此這些組件本身必須宣告它們自己在哪個 scope
例如 @Singleton 跟 @RequestScoped 不會 binding 在同一個 component
因為不同 scope 有不同生命週期,所以無法同時生活在不同生命周期裡

要宣告 component 與哪個 scope 相關只要加上該 scope 的標註就可以

@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
  CoffeeMaker maker();
}


* Lazy injections

有時你會需要一個可以延遲實體化的物件
binding T 型別我們可以用 Lazy<T>
它會等到 Lazy<T> 首次呼叫 get() 方法才建立實體
如果 T 是一個 singleton,那麼 Lazy<T> 在所有注入的 graph 上會是同一個實體
否則每個注入都會取得各自的 Lazy<T> 實體

無論如何,之後任何 Lazy<T> 實體的呼叫都會回傳同一個基本的 T 實體
class GridingCoffeeMaker {
  @Inject Lazy lazyGrinder;

  public void brew() {
    while (needsGrinding()) {
      // Grinder created once on first call to .get() and cached.
      lazyGrinder.get().grind();
    }
  }
}

* Provider injections

有時你會需要注入單一值而取得多個實體
這有幾個方式可以做(例如 Factories, Builders)
其中一個選擇是用 Provider<T>
Provider<T> 在每次呼叫 get() 時都會使用 T 的 binding 邏輯
如果 binding 邏輯是 @Inject 的建構子,就會回傳一個新建構的實體
但 @Provides method 則不一定

class BigCoffeeMaker {
  @Inject Provider<filter> filterProvider;

  public void brew(int numberOfPots) {
  ...
    for (int p = 0; p < numberOfPots; p++) {
      maker.addFilter(filterProvider.get()); //new filter every time.
      maker.addCoffee(...);
      maker.percolate();
      ...
    }
  }
}
注入 Provider<T> 可能會創造出易混淆的程式碼而且在 graph 中造成範圍錯誤 (mis-scoped) 或建構錯誤 (mis-structured) 的物件
通常你會想使用 factory 或 Lazy<T> 或重整生命週期及建構程式的方法,去達成 只注入 T 的目的
但注入 Provider<T> 在某些情況下可能成為救命稻草
一個常見的情況是當你必須使用傳統的架構,不與你物件的生命週期有關連
(例如 servlet 被設計為 singleton,但只有在請求特定資料時才有效 )

* Qualifiers

當 class type 不足以識別依賴性時我們可以用 @Qualifier
例如在 Android 我們會需要不同類型的 context
所以我們可以定義 @ForApplication 跟 @ForActivity” 標註
那當注入一個 context 時我們可以使用這些標註去告訴 Dagger 哪一種 context 是我們所需要的
你可以用 @Qualifier 標在任何標註上

例如一個複雜的咖啡機可能會把加熱器分為水的跟熱盤用的
所以 qualifier annotation 派上用場了
下例是 javax.inject 裡的 @Named 標註
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
  String value() default "";
}
你可以創造自己的 Qualifier 或使用原本的 @Named
但一個依賴性無法有多個 qualifier 標註
class ExpensiveCoffeeMaker {
  @Inject @Named("water") Heater waterHeater;
  @Inject @Named("hot plate") Heater hotPlateHeater;
  ...
}
@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
  return new ElectricHeater(70);
}

@Provides @Named("water") static Heater provideWaterHeater() {
  return new ElectricHeater(93);
}
* Compile-time Validation

Dagger 的 annotation processor 是嚴格的
如果有任何不合法或不完整的 binding 就會造成 compiler error
下例 component 裡的 module,Executor 缺少了一個 binding
@Module
class DripCoffeeModule {
  @Provides static Heater provideHeater(Executor executor) {
    return new CpuHeater(executor);
  }
}
[ERROR] COMPILATION ERROR :
[ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.
這個問題只要在 component 裡的任意 modules 的 Executor 加上 @Provides 標註即可
雖然 @Inject, @Module 與 @Provides 標註是被各別驗證
所有 binding 間的關聯驗證發生在 @Component 層
Dagger 1 依賴嚴格的 @Module 層驗證,但 Dagger 2 這樣的驗證(及 @Module 的配置參數)有利於全物件圖驗證

* Compile-time Code Generation

Dagger 的 annotation processor 也會生成名稱像是 CoffeeMaker_Factory.java 或 CoffeeMaker_MembersInjector.java 這樣的原始檔
這些檔案是 Dagger 實作細節
不需要直接使用它們,因為它們可以在 injection 中逐步除錯
唯一需要參考的是 Dagger 前綴的那些 component 類別

相關資料:

YouTube-DAGGER 2 - A New Type of dependency injection (slides) (coffee example)
YouTube-Dependency Injection Using Dagger 2
Dagger
Dagger & Android
Tasting Dagger 2 on Android

沒有留言: