Dagger2-User's Guide

V2.16


Summary

在任何應用程序中,最好的類都是那些做事情的類:BarcodeDecoder、KoopaPhysicsEngine和AudioStreamer。這些類有依賴性;可能是BarcodeCameraFinder、DefaultPhysicsEngine和HttpStreamer。

相比之下,任何應用程序中最糟糕的類都是那些占用空間卻不做太多事情的類:BarcodeDecoderFactory、CameraServiceLoader和MutableContextWrapper。這些類就像笨拙的管道膠帶,將有趣的東西連接在一起。

Dagger是對這些FactoryFactory類的替換,它實現了依賴注入設計模式,無需編寫模板文件。它可以讓你專注于有趣的類。聲明依賴項,指定如何滿足它們,并發布應用程序。

通過構建標準的javax-inject注釋(JSR 330),每個類都易于測試。你不需要大量的模板就可以將RpcCreditCardService轉換為FakeCreditCardService。

依賴注入不僅僅用于測試。 它還可以輕松創建可重用、可互換的模塊。 您可以在所有應用程序中共享相同的AuthenticationModule。 您可以在開發期間運行DevLoggingModule,在生產中運行ProdLoggingModule,以獲得每種情況下的正確行為。

Why Dagger 2 is Different

依賴注入框架已經存在多年了,它使用各種各樣的api進行配置和注入。那么,為什么要重新發明輪子呢?Dagger 2是第一個使用生成的代碼實現完整堆棧的工具。指導原則是生成代碼,該代碼模仿用戶可能手寫的代碼,以確保依賴注入盡可能簡單、可跟蹤和高性能。要了解更多的設計背景,請觀看+Gregory Kick的演講(幻燈片)。

Using Dagger

我們將通過構建一個咖啡機來演示依賴注入和Dagger。有關可以編譯和運行的完整示例代碼,請參見Dagger的咖啡示例。

Declaring Dependencies

Dagger構造應用程序類的實例并滿足它們的依賴關系。 它使用javax.inject.Inject注釋來標識它感興趣的構造函數和字段。

使用@Inject來注釋Dagger應該用來創建類實例的構造函數。 當一個新的實例被請求時,Dagger將獲得所需的參數值并調用這個構造函數。

class Thermosiphon implements Pump { private final Heater heater; @Inject Thermosiphon(Heater heater) { this.heater = heater; } ... }

Dagger可以直接注入字段。在本例中,它獲得了Heater的實例heater和Pump的實例pump。

class CoffeeMaker { @Inject Heater heater; @Inject Pump pump; ... }

如果您的類具有@ Inject注釋的字段,但沒有@注入注釋的構造函數,Dagger會根據請求注入這些字段,但不會創建新的實例。 使用@Inject注釋添加一個無參數構造函數,以指示Dagger也可以創建實例。

Dagger也支持方法注入,雖然通常建議使用構造函數或字段注入。

缺少@Inject注釋的類不能由Dagger構造。

Satisfying Dependencies

在默認情況下,Dagger通過構造上面描述的請求類型的實例來滿足每個依賴項。當您請求使用CoffeeMaker時,它將通過調用new CoffeeMaker()并設置其可注射字段來獲得。

但是@Inject不是在任何地方都能用的:

????接口不能建造。

????第三方類不能被注釋。

????必須配置可配置的對象!

對于那些@Inject不夠或不方便的情況,可以使用@provides注釋方法來滿足依賴關系。方法的返回類型定義了它滿足的依賴關系。

例如,當需要Heater時,就調用provideHeater()?:

@Provides static Heater provideHeater() { return new ElectricHeater(); }

@Provides方法也可以有它們自己的依賴關系。每當需要Pump時,這個系統就會返回一個Thermosiphon:

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

所有@Provides方法都必須屬于一個module(模塊)。 這些只是具有@Module注釋的類。

@Module class DripCoffeeModule { @Provides static Heater provideHeater() { return new ElectricHeater(); } @Provides static Pump providePump(Thermosiphon pump) { return pump; } }

按照慣例,@Provides方法以提供前綴命名,模塊類以Module后綴命名。

Building the Graph

@Inject和@ provides注釋類由對象的依賴項鏈接而成。像應用程序的main方法或Android應用程序那樣調用代碼,通過一組定義良好的根訪問該圖形。在Dagger 2中,該集合由一個接口定義,該接口沒有參數并返回所需類型。通過將@Component注釋應用于這樣的接口并將模塊類型傳遞給modules參數,Dagger 2就可以完成生成該契約的實現。

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

該實現的名稱與以Dagger為前綴的接口名稱相同。 通過在該實現上調用builder()方法獲取實例,并使用返回的構造器來設置依賴項并build()一個新實例。

CoffeeShop coffeeShop = DaggerCoffeeShop.builder() .dripCoffeeModule(new DripCoffeeModule()) .build();

Note:?如果您的@Component不是頂層類型,則生成的組件的名稱將包含其封閉類型的名稱,并加上下劃線。 例如,這段代碼:

class Foo { static class Bar { @Component interface BazComponent {} } }

將生成一個名為DaggerFoo_Bar_BazComponent的組件。

任何具有可訪問默認構造函數的模塊都可以省略,因為如果沒有設置,構建器將自動構造一個實例。 對于任何其@Provides方法都是靜態的模塊,實現根本不需要實例。 如果所有的依賴都可以在沒有用戶創建依賴實例的情況下構建,那么生成的實現也會有一個create()方法,該方法可用來獲取新實例而無需處理構建器。

CoffeeShop coffeeShop = DaggerCoffeeShop.create();

現在,我們的CoffeeApp可以簡單地使用dagger生成的CoffeeShop實例來獲得一個完全注入的CoffeeMaker。

public class CoffeeApp { public static void main(String[] args) { CoffeeShop coffeeShop = DaggerCoffeeShop.create(); coffeeShop.maker().brew(); } }

現在已經構建好了圖形,并注入了入口點,我們可以運行我們的coffee maker應用程序了。

$ java -cp ... coffee.CoffeeApp ~ ~ ~ heating ~ ~ ~ => => pumping => => [_]P coffee! [_]P

Bindings in the graph

上面的例子展示了如何使用一些更典型的綁定來構建一個組件,還有多種機制可以為圖形提供綁定。 以下是依賴關系,可用于生成格式良好的組件:

? ??那些由@Provides聲明在@Module中的方法由@Component.modules直接引用,或者通過@ Module.includes傳遞。

? ??任何帶有@Inject構造函數的類型,該類型是unscoped的,或具有與組件范圍之一相匹配的Scope注釋。

????為組件依賴關系的組件提供方法。

? ??組件本身。

? ??任何包含子組件(subcomponent)的均為不合格的建設者。

? ??Provider或Lazy包裝器用于上述任何綁定。

? ??任何上述綁定的Lazy提供者(例如,Provider<Lazy<CoffeeMaker>>)。

? ??任何類型的MembersInjector。

Singletons and Scoped Bindings

使用@Singleton注釋的@Provides方法或注入類。 該圖形(graph)將為其所有客戶端使用該值的單個實例。

@Provides @Singleton static Heater provideHeater() { return new ElectricHeater(); }

注入類上的@Singleton注解也可以作為文檔。 它提醒潛在的維護者,這個類可以被多個線程共享。

@Singleton class CoffeeMaker { ... }

由于Dagger 2將圖(graph)中的作用域實例(scope)與組件實現的實例關聯,因此組件本身需要聲明它們打算表示的作用域。例如,在同一個組件中有一個@Singleton綁定和一個@RequestScoped綁定沒有任何意義,因為這些作用域有不同的生命周期,因此必須存在于具有不同生命周期的組件中。要聲明組件與給定范圍相關聯,只需將范圍注釋應用到組件接口。

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

組件可能會應用多個范圍注釋。 這聲明它們都是相同范圍的別名,并且組件可以包含它聲明的任何范圍的范圍綁定。

Reusable scope

有時,您希望限制實例化@ Inject類或調用@ provider方法的次數,但不需要保證在任何特定組件或子組件的生命周期中使用相同的實例。這在Android這樣的環境中非常有用,因為在這些環境中,分配可能會很昂貴。

對于這些綁定,您可以應用@Reusable范圍。 與其他范圍不同,@Reusable范圍的綁定不與任何單個組件關聯;相反,實際使用綁定的每個組件都會緩存返回的或實例化的對象。

這意味著,如果您在組件中安裝了一個具有@Reusable綁定的模塊,但實際上只有子組件使用綁定,那么只有子組件將緩存綁定的對象。如果沒有共享祖先的兩個子組件各自使用綁定,那么每個子組件都將緩存自己的對象。如果組件的祖先已經緩存了對象,子組件將重用它。

不能保證組件只調用綁定一次,因此將@Reusable應用于返回可變對象的綁定,或者是引用相同實例的對象,這是很危險的。如果您不關心分配了多少次不可變對象,那么可以安全地為它們使用@Reusable。

@Reusable // It doesn't matter how many scoopers we use, but don't waste them. class CoffeeScooper { @Inject CoffeeScooper() {} } @Module class CashRegisterModule { @Provides @Reusable // DON'T DO THIS! You do care which register you put your cash in. // Use a specific scope instead. static CashRegister badIdeaCashRegister() { return new CashRegister(); } } @Reusable // DON'T DO THIS! You really do want a new filter each time, so this // should be unscoped. class CoffeeFilter { @Inject CoffeeFilter() {} }

Lazy injections

有時你需要一個對象懶惰地實例化。 對于任何綁定T,您可以創建一個Lazy,它延遲實例化,直到首次調用Lazy的get()方法。 如果T是一個單例,那么Lazy將成為ObjectGraph內所有注入的相同實例。 否則,每個注入站點將獲得它自己的Lazy實例。 無論如何,對Lazy的任何給定實例的后續調用將返回相同的T的底層實例。

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

Provider injections

有時你需要返回多個實例,而不是只注入一個值。 雖然有幾個選項(工廠,構建器等),但一種選擇是注入一個Provider<T>而不僅僅是T。一個Provider<T>每次調用.get()時調用T的綁定邏輯。 如果該綁定邏輯是@Inject構造函數,則會創建一個新實例,但@Provides方法沒有這種保證。

class BigCoffeeMaker { @Inject Provider 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可能會產生混淆的代碼,并且可能是圖形中存在錯誤或錯誤結構對象的設計氣味。 通常你會想要使用工廠或Lazy或者重新組織代碼的生命周期和結構,以便能夠注入T.注入提供者可以在某些情況下成為救命。 一個常見的用法是,您必須使用與對象的自然生存期不一致的遺留體系結構(例如,servlet按照設計是單件的,但僅在請求特定數據的上下文中有效)。

Qualifiers

有時僅僅這種類型不足以識別依賴性。 例如,一個復雜的咖啡機應用程序可能需要為水和熱板分開加熱器。

在這種情況下,我們添加一個限定符注釋。 這是任何注釋本身有一個@Qualifier注釋。 以下是@Named的聲明,它是javax.inject中包含的限定符注釋:

@Qualifier @Documented @Retention(RUNTIME) public @interface Named { String value() default ""; }

您可以創建自己的限定符注釋,或者僅使用@Named。 通過注釋感興趣的字段或參數來應用限定符。 類型和限定符注釋都將用于標識依賴關系。

class ExpensiveCoffeeMaker { @Inject @Named("water") Heater waterHeater; @Inject @Named("hot plate") Heater hotPlateHeater; ... }

通過注釋相應的@Provides方法來提供合格的值。

@Provides @Named("hot plate") static Heater provideHotPlateHeater() { return new ElectricHeater(70); }

@Provides @Named("water") static Heater provideWaterHeater() { return new ElectricHeater(93); }

依賴項可能沒有多個限定符注釋。

Optional bindings

如果你想讓綁定工作,即使組件中沒有綁定某個依賴關系,也可以在模塊中添加一個@BindsOptionalOf方法:

@BindsOptionalOf abstract CoffeeCozy optionalCozy();

這意味著@Inject構造函數和成員和@Provides方法可以依賴于一個可選的對象。 如果組件中存在CoffeeCozy的綁定,則可選將存在; 如果CoffeeCozy沒有綁定,則可選將不存在。

具體而言,您可以注入以下任何一項:

????????Optional?(unless there is a?@Nullable?binding for?CoffeeCozy; see below)

????????Optional>

????????Optional>

????????Optional>>

(你也可以注入一個提供者或者懶惰或者懶惰的提供者,但這不是很有用。)

如果CoffeeCozy有一個綁定,并且該綁定是@Nullable,那么注入可選是一個編譯時錯誤,因為Optional不能包含null。 您可以隨時注入其他表單,因為Provider和Lazy總是可以從get()方法返回null。

如果子組件包含對基礎類型的綁定,則可以在子組件中存在一個組件中不存在的可選綁定。

您可以使用Guava的Optional或Java 8的Optional。

Binding Instances

在構建組件時,您經??梢垣@得可用的數據。 例如,假設您有一個使用命令行參數的應用程序; 你可能想要在你的組件中綁定這些參數。

也許你的應用程序只需要一個參數來表示你想作為@UserName String注入的用戶名。 您可以將一個注釋@BindsInstance的方法添加到組件構建器,以允許將該實例注入組件。

@Component(modules = AppModule.class) interface AppComponent { App app(); @Component.Builder interface Builder { @BindsInstance Builder userName(@UserName String userName); AppComponent build(); } }

你的應用程序可能會是這樣:

public static void main(String[] args) { if (args.length > 1) { exit(1); } App app = DaggerAppComponent .builder() .userName(args[0]) .build() .app(); app.run(); }

在上面的例子中,在組件中注入@UserName String將在調用此方法時使用提供給Builder的實例。 在構建組件之前,必須調用所有@BindsInstance方法,并傳遞一個非空值(除了下面的@Nullable綁定外)。

如果@BindsInstance方法的參數被標記為@Nullable,那么綁定將被認為是“可空的”,就像@Provides方法是可空的那樣:注入站點也必須將其標記為@Nullable,并且null是可接受的值 綁定。 而且,Builder的用戶可能會省略調用該方法,并且該組件會將該實例視為null。

@BindsInstance方法應該優先于用構造函數參數編寫@Module并立即提供這些值。

Compile-time Validation

Dagger注釋處理器是嚴格的,如果任何綁定無效或不完整,將導致編譯器錯誤。 例如,該模塊安裝在缺少Executor綁定的組件中:

@Module class DripCoffeeModule { @Provides static Heater provideHeater(Executor executor) { return new CpuHeater(executor); } }

編譯時,javac會拒絕缺少的綁定:

[ERROR] COMPILATION ERROR : [ERROR] error: java.util.concurrent.Executor cannot be provided without an @Provides-annotated method.

通過為Executor添加一個@ Provide-annotated方法來修復組件中的任何模塊。 雖然@Inject,@Module和@Provides注釋是單獨驗證的,但綁定之間的關系的所有驗證發生在@Component級別。 Dagger 1嚴格依賴于@模塊級驗證(可能會或可能不會反映運行時行為),但Dagger 2將此類驗證(以及@Module上隨附的配置參數)取消為全圖驗證。

Compile-time Code Generation

Dagger的注釋處理器也可以生成帶有CoffeeMaker_Factory.java或CoffeeMaker_MembersInjector.java等名稱的源文件。 這些文件是Dagger實現細節。 您不需要直接使用它們,但通過注入進行分步調試時,它們可能非常方便。 您應該在您的代碼中引用的唯一生成的類型是為您的組件添加了Dagger的前綴。



后記:

Dagger的GitHub,翻譯原文傳送門

一篇非常不錯的dagger2入門文章:Dagger2從入門到放棄再到恍然大悟。

dagger2的中文資料沒有太多,dagger-android的資料更小稀少,本來還想翻譯完后再翻譯一下Dagger & Android的,這會可能要重新考慮了。

谷歌官方的Android架構Demo,我是為了看懂里面的MVP+Dagger2架構,才走上學習和翻譯官網的這條路,谷歌里面的架構的確值得我們學習。

翻譯???并不好,甚至可能晦澀難懂,對dagger2 研究和應用會繼續進行。歡迎各路大神不吝賜教~!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容