Dagger2
轉(zhuǎn)載請注明原作者纹坐,如果你覺得這篇文章對你有幫助或啟發(fā),可以關(guān)注打賞舞丛。
前言
本文翻譯自Google Dagger2文檔耘子,才疏學(xué)淺,歡迎拍磚球切,希望能幫到你谷誓。
架構(gòu)方面請關(guān)注GitHub(MVP+Retrofit+Dagger2+Okhttp)及我的文章Android UI框架快速搭建實踐。
Dagger2原理分析請關(guān)注Dagger2詳解-從代碼分析其原理吨凑。
友情提示捍歪,因為簡書markdown頁內(nèi)跳轉(zhuǎn)支持問題,目錄及其他頁內(nèi)跳轉(zhuǎn)位置點擊后會在瀏覽器打開新的tab鸵钝,且不能跳轉(zhuǎn)到相應(yīng)位置糙臼,希望不會對你造成困擾。
<p id="top">目錄</p>
Home
User's Guide
Android
Multibinding Sets and Maps
Subcomponents
Producers
Testing
Project Pages
<p id="home">Home</p>
Dagger是一個完全靜態(tài)的編譯時的Java和Android依賴注入礦建恩商。它由Square發(fā)布的早期版本改造而來現(xiàn)在由Google維護变逃。
Dagger致力于解決開發(fā)中使用基于反射的的解決方案帶來的性能問題。更多詳情可以在這里找到 by +Gregory Kick.
文檔
代碼
有問題怠堪?
- 在Stack Overflow dagger-2專題下提問
- 發(fā)送郵件到dagger-discuss@googlegroups.com
<p id="guide">User's Guide</p>
應(yīng)用中好的類是那些做了事情實現(xiàn)了功能的類揽乱,例如:BarcodeDecoder名眉、KoopaPhysicsEngine和AudioStreamer。這些類可能依賴了其他類凰棉,如:BarcodeCameraFinder损拢、DefaultPhysicsEngine及HttpStreamer。
相比之下渊啰,應(yīng)用中那些糟糕的類往往占用空間卻沒做什么事探橱,例如:BarcodeDecoderFactory、CameraServiceLoader以及MutableContextWrapper绘证,這些類就像膠帶一樣笨拙地將相關(guān)的東西捆在一起。
實現(xiàn)了依賴注入設(shè)計模式而且不需要書寫模板的Dagger是這些工廠類的替代品哗讥。它可以讓你專注于你感興趣的類嚷那。聲明依賴,然后指定怎么注入它們杆煞,然后發(fā)布你的應(yīng)用魏宽。
構(gòu)建基于標準javax.inject注解(JSR 330),類的測試更容易决乎。你不需要寫一大堆樣板文件只需要用FakeCreditCardService替換RpcCreditCardService即可队询。
依賴注入不僅僅應(yīng)用于測試。它還便于創(chuàng)建通用的构诚,可復(fù)用的模塊蚌斩。你可以在你所有的應(yīng)用共享一個AuthenticationModule。你還可以在開發(fā)環(huán)境中運行DevLoggingModule范嘱,在生產(chǎn)環(huán)境中運行ProdLoggingModule以在不同情景下都能達到正確的行為送膳。
<p id="jump">Dagger2的亮點 </p>
依賴注入的框架已經(jīng)存在好多年了并且擁有多種多樣的API來配置和注入。那為什么要重新造輪子呢丑蛤?Dagger2是第一個使用生成的代碼實現(xiàn)全棧的依賴注入框架叠聋。指導(dǎo)原則是模仿用戶手寫的代碼來生成代碼盡可能地保證依賴注入過程簡單、可追蹤受裹、高性能碌补。想了解更多關(guān)于這種設(shè)計的信息請觀看視頻(幻燈片) by +Gregory Kick。
Using Dagger
我們將通過建造一個咖啡機的過程來演示依賴注入和Dagger.可編譯運行的完整樣例代碼請看Dagger的咖啡樣例棉饶。
聲明依賴
Dagger構(gòu)建你的應(yīng)用中的實例并滿足他們的依賴厦章。它使用javax.inject.Inject注解來標識感興趣的構(gòu)造器和字段。
使用@Inject注解告訴Dagger創(chuàng)建類的實例應(yīng)該用的構(gòu)造器砰盐。當需要一個實例對象時闷袒,Dagger將會獲得需要的參數(shù)并調(diào)用這個構(gòu)造器。
class Thermosiphon implements Pump {
private final Heater heater;
@Inject
Thermosiphon(Heater heater) {
this.heater = heater;
}
...
}
Dagger 可以直接注入字段(成員變量)岩梳。在這個例子中囊骤,它拿到Heater類實例對象和Pump類實例對象分別賦值給CoffeeMaker的heater字段和pump字段晃择。
class CoffeeMaker {
@Inject Heater heater;
@Inject Pump pump;
...
}
如果你的類有使用@Inject注解的字段但沒有@Inject注解了的構(gòu)造器與之對應(yīng),Dagger會注入這些字段但不會創(chuàng)建新的實例也物。添加一個帶有@Inject注解的無參構(gòu)造器來告訴Dagger可以創(chuàng)建對象宫屠。
Dagger也支持方法注入,但更推薦構(gòu)造器注入和字段注入滑蚯。
缺少@Inject注解的類是不能被Dagger構(gòu)建的浪蹂。
實現(xiàn)依賴
默認情況下,Dagger會通過創(chuàng)建需要類型的實例來提供依賴告材。當你需要一個CoffeeMaker坤次,它會通過new CoffeeMaker()來獲得一個實例并賦值給需要注入的字段。
但@Inject不是哪都有效的:
- 接口不能被創(chuàng)建(不支持接口注入)
- 第三方的類不能被注解(第三方類沒有@Inject注解斥赋,除非可以改源碼)
- 可配置的對象必須配置好(這個應(yīng)該是泛型缰猴,本人在使用時發(fā)現(xiàn)注入是不支持泛型的)
在上面這些場景下@Inject就有些尷尬了,使用@Provides注解方法來實現(xiàn)依賴疤剑。方法的返回類型與其要實現(xiàn)的依賴一致滑绒。
例如:只要需要Heater實例就會調(diào)用provideHeater()方法。
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
@Provides標注的方法可以依賴他們自己隘膘。任何時候當需要Pump對象時疑故,下面這個方法返回一個Thermosiphon對象。
所有的@Provides注解的方法必須屬于一個Module.這些類只是有@Module注解的類弯菊。
@Module
class DripCoffeeModule {
@Provides static Heater provideHeater() {
return new ElectricHeater();
}
@Provides static Pump providePump(Thermosiphon pump) {
return pump;
}
}
按照慣例纵势,@Provides方法命名帶provide前綴,module類命名帶Module后綴误续。
建立對象圖
@Inject和@Provides注解的類通過他們的依賴關(guān)系聯(lián)系起來形成對象圖吨悍。調(diào)用代碼就像一個應(yīng)用的main方法或者Android應(yīng)用通過一組明確定義的根集訪問那個對象圖。Dagger2中蹋嵌,那個集合是由一個包含返回需要類型且無參的方法的接口定義育瓜。通過@Component注解這個接口并傳入module類型的參數(shù),Dagger2然后根據(jù)這個協(xié)議生成所有實現(xiàn)栽烂。
@Component(modules = DripCoffeeModule.class)
interface CoffeeShop {
CoffeeMaker maker();
}
實現(xiàn)類的類名與接口的名字加上Dagger前綴相同躏仇。通過調(diào)用實現(xiàn)類的builder()方法可以獲得Builder實例,通過這個實例可以設(shè)置依賴并build()得到一個新的實例腺办。
CoffeeShop coffeeShop = DaggerCoffeeShop.builder()
.dripCoffeeModule(new DripCoffeeModule())
.build();
Note:如果你的@Component不是頂層的類型焰手,那生成的component的名字將會包含它的封閉類型的名字,通過下劃線連接怀喉。例如:
class Foo {
static class Bar {
@Component
interface BazComponent {}
}
}
將會生成名為DaggerFoo_ Bar_BazComponent的component.
任何有可達的默認構(gòu)造器的module都可以被省略书妻,如果沒有設(shè)置builder會自動創(chuàng)建一個實例。而且任何@Provides方法都是靜態(tài)的module,builder是不需要其實例的躬拢。如果不需要用戶創(chuàng)建依賴實例就可以創(chuàng)建所有的依賴躲履,那么生成的實現(xiàn)類將會包含一個create()方法见间,可以使用此方法得到一個實例而不用于builder打交道。
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
現(xiàn)在工猜,我們的CoffeeApp可以方便的通過Dagger生成的實現(xiàn)來得到一個完全注入的CoffeeMaker.
public class CoffeeApp {
public static void main(String[] args) {
CoffeeShop coffeeShop = DaggerCoffeeShop.create();
coffeeShop.maker().brew();
}
}
現(xiàn)在圖已經(jīng)建立了入口也已經(jīng)注入了米诉,我們開啟我們的咖啡機應(yīng)用。
$ java -cp ... coffee.CoffeeApp
~ ~ ~ heating ~ ~ ~
=> => pumping => =>
[_]P coffee! [_]P
對象圖中的綁定
上面的例子展現(xiàn)如何構(gòu)建一個擁有一些典型綁定的component篷帅,但還有不同的機制來為圖貢獻綁定史侣。作為依賴下面這些是可用的而且可以用來生成更好的component.
- 這些@Module中由@Provides聲明的方法可以直接被@Component.modules或@Module.includes引用
- 任何類型的@Inject注解的構(gòu)造器可以沒有作用域也可以有與某個Component作用域一致的@Scope注解
- component依賴的component提供方法
- component自己
- 任何包含的subcomponent的不合格的builders
- 上面所有綁定的Provider和Lazy 包裝器。
- 上面綁定的懶加載的供應(yīng)器(e.g Provider<Lazy<CoffeeMaker>>)
- 任何類型的MemberInjector
單例和域綁定
用@Singleton注解@Provide方法或可注入的類魏身,對象圖將會在應(yīng)用中使用同一個一個實例惊橱。
@Provides @Singleton static Heater provideHeater() {
return new ElectricHeater();
}
可注入的類上的@Singleton注解也可以作為文檔。它告訴潛在的維護者這個類可能被多個線程共享叠骑。
@Singleton
class CoffeeMaker {
...
}
因為Dagger2會將圖中添加了作用域的實例和component實現(xiàn)類的實例聯(lián)系起來李皇,所以這些component需要聲明作用域。例如:在同一個component中使用@Singleton和@RequestScoped是沒有意義的宙枷,因為他們具有不同的生命周期。想要聲明一個具有作用域的component茧跋,只需要在該接口添加域注解慰丛。
@Component(modules = DripCoffeeModule.class)
@Singleton
interface CoffeeShop {
CoffeeMaker maker();
}
Components可以有多種域注解。表明這些注解是同一個域的別名瘾杭,這樣component就可以包含它聲明的域的所有綁定了诅病。
可重用的scope
有時你可能想限制@Inject注解的構(gòu)造器初始化的次數(shù)或者@Provides方法被調(diào)用的次數(shù),但并不需要保證單例粥烁。這在內(nèi)存比較吃緊的環(huán)境比如Android下會很有用贤笆。
當你使用@Resuable注解,這些@Resuable域綁定不像其他的域讨阻,不會和任何component聯(lián)系芥永,相反,每個使用這個綁定component會將返回值或初始化的對象緩存起來钝吮。
這意味著如果你在component中裝載了@Resuable綁定的module,但只有一個子component使用了埋涧,那么那個子component將會緩存此綁定的對象。如果兩個子component都使用了這個綁定但他們不繼承同一個component奇瘦,那么這兩個子component的緩存是獨立的棘催。如果component已經(jīng)緩存了對象,其子component會重用該對象耳标。
并不能保證component只會調(diào)用該綁定一次醇坝,所以在返回可變對象或者需要使用單例的綁定上使用@Resuable是很危險的。對不關(guān)心被分配多少次的不變對象使用@Resuable是安全的次坡。
@Reusable // 我們用了多少scopers并不重要呼猪,但不要浪費他們画畅。
class CoffeeScooper {
@Inject CoffeeScooper() {}
}
@Module
class CashRegisterModule {
@Provides
@Reusable // 不要這樣做!你是關(guān)注你保存cash的register的
// Use a specific scope instead.
static CashRegister badIdeaCashRegister() {
return new CashRegister();
}
}
@Reusable // 不要這樣做郑叠! 你實際想每次都拿到新的filter對象夜赵,所以這里不需要使用域。
class CoffeeFilter {
@Inject CoffeeFilter() {}
}
延遲注入
有時你需要延遲初始化對象乡革。對于任意綁定T等孵,你可以創(chuàng)建Lazy<T>到千,這樣就可以延遲對象初始化直到調(diào)用Lazy<T>的get()方法。如果T是單例的,那么在對象圖中所有的注入都是同一個Lazy<T>實例乖阵。否則每個注入拿到的都是自己的Lazy<T>實例。對同一個Lazy<T>實例連續(xù)調(diào)用get()方法返回的都是一個T對象采桃。
class GridingCoffeeMaker {
@Inject Lazy<Grinder> lazyGrinder;
public void brew() {
while (needsGrinding()) {
//第一次調(diào)用get()時會創(chuàng)建Grinder對象并緩存起來
lazyGrinder.get().grind();
}
}
}
Provider注入
有時你需要返回多個實例而不是注入單個值土浸。你有多種選擇(Factories,Builders,等等),其中一種選擇就是注入一個Provider<T>而不是T。每次調(diào)用get()方法時Provider<T>會調(diào)用綁定邏輯蕾殴。如果那個綁定邏輯是@Inject注解的構(gòu)造器笑撞,會創(chuàng)建一個新對象,但一個@Provides方法是無法保證這點的钓觉。
class BigCoffeeMaker {
@Inject Provider<Filter> filterProvider;
public void brew(int numberOfPots) {
...
for (int p = 0; p < numberOfPots; p++) {
maker.addFilter(filterProvider.get()); //每次都是新的filter對象
maker.addCoffee(...);
maker.percolate();
...
}
}
}
Note:注入Provider<T>可能降低代碼的可讀性茴肥。通常你會用一個factory或一個Lazy<T>或是重新組織代碼的結(jié)構(gòu)和生命周期來注入一個T。但注入Provider<T>有些情況可以救命荡灾。一個通常的使用場景就是當你必須使用一個遺留的并不與你的對象的自然生命周期一樣的架構(gòu)時瓤狐。(例如:按照設(shè)計servlets是單例的,但只有在明確請求數(shù)據(jù)的上下文中中是有效的)批幌。
Qualifiers
有時類型不足以區(qū)分依賴础锐。例如:一個復(fù)雜的咖啡機想要將睡和盤子的加熱器分開。
這種情況荧缘,我們添加一個qualifier annotation皆警。這是任何本身有@Qualifier注解的注解。下面是@Named的聲明胜宇,它是javax.inject中的注解耀怜。
@Qualifier
@Documented
@Retention(RUNTIME)
public @interface Named {
String value() default "";
}
你可以創(chuàng)建自定義的qualifier注解,或使用@Named.在關(guān)心的字段或參數(shù)上使用qualifiers.類型+qualifier將會用來標識一個依賴桐愉。
class ExpensiveCoffeeMaker {
@Inject @Named("water") Heater waterHeater;
@Inject @Named("hot plate") Heater hotPlateHeater;
...
}
注解對應(yīng)的@Provides方法來提供限定的值财破。
@Provides @Named("hot plate") static Heater provideHotPlateHeater() {
return new ElectricHeater(70);
}
@Provides @Named("water") static Heater provideWaterHeater() {
return new ElectricHeater(93);
}
依賴可以有多個qualifier注解。
編譯期校驗
Dagger的注解處理器會生成名如CoffeeMaker_Factory.java或CoffeeMaker_MembersInjector.java的源文件从诲。這些文件就是Dagger的實現(xiàn)細節(jié)左痢。你不需要直接使用它們,雖然通過注解單步調(diào)試時會很方便。唯一需要你關(guān)心的是那些帶有Dagger前綴的為component生成的代碼俊性。
Using Dagger In Your Build
Gradle Users
你需要引入運行時依賴dagger-2.2.jar略步,為了激活代碼生成還要引入編譯期依賴dagger-compiler-2.2.jar.
Maven工程在pom.xml如下配置:
<dependencies>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger</artifactId>
<version>2.2</version>
</dependency>
<dependency>
<groupId>com.google.dagger</groupId>
<artifactId>dagger-compiler</artifactId>
<version>2.2</version>
<optional>true</optional>
</dependency>
</dependencies>
Android Gradle
// Add plugin https://bitbucket.org/hvisser/android-apt
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}
}
// Apply plugin
apply plugin: 'com.neenbedankt.android-apt'
// Add Dagger dependencies
dependencies {
compile 'com.google.dagger:dagger:2.x'
apt 'com.google.dagger:dagger-compiler:2.x'
}
<p id="android">Dagger&Android</p>
相對于其他依賴注入框架Dagger2最基本的優(yōu)勢之一就是完全生成代碼(沒有反射),這意味著它是可以應(yīng)用于Anroid的定页。但當使用時還是有一些需要注意的地方趟薄。
原理(Philosophy)
因為針對Android編寫的代碼是Java代碼,所以在風(fēng)格上常有很大區(qū)別典徊,這種差異的存在以適應(yīng)Android平臺獨特性能考慮杭煎。
為了生成通用且輕便的代碼,Dagger基于ProGuard對編譯后的字節(jié)碼進行后期處理卒落。這樣Dagger可以產(chǎn)生在server和Android上都很自然的代碼羡铲,使用不同的工具鏈是生成的字節(jié)碼在兩個環(huán)境下都能高效執(zhí)行。此外儡毕,Dagger可以明確保證其生成的Java代碼在ProGuard優(yōu)化后是可編譯也切。
<p id="multibindings">多重綁定(Multibindings)</p>
Dagger支持將多個對象綁定進一個集合即使這些對象已經(jīng)綁定在不同的module中。
你可以使用多重綁定實現(xiàn)插件架構(gòu)腰湾,例如:不同的module都可以貢獻自己對插件接口的實現(xiàn)這樣中央類就可以使用這些插件集合雷恃。或者你可以有多個module貢獻各自的service providers费坊,以名稱為鍵保存在一個map中褂萧。
Set multibindings
在module的方法上添加@IntoSet注解,向一個可注入的多重綁定的set貢獻元素葵萎。
@Module
class MyModuleA {
@Provides @IntoSet
static String provideOneString(DepA depA, DepB depB) {
return "ABC";
}
}
你還可以在返回值為集合的方法使用@ElementsIntoSet注解來同時貢獻多個元素。
@Module
class MyModuleB {
@Provides @ElementsIntoSet
static Set<String> provideSomeStrings(DepA depA, DepB depB) {
return new HashSet<String>(Arrays.asList("DEF", "GHI"));
}
}
現(xiàn)在component的一個綁定可以依賴這個set了:
class Bar {
@Inject Bar(Set<String> strings) {
assert strings.contains("ABC");
assert strings.contains("DEF");
assert strings.contains("GHI");
}
}
component也可以提供這個set:
@Component(modules = {MyModuleA.class, MyModuleB.class})
interface MyComponent {
Set<String> strings();
}
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.strings()).containsExactly("ABC", "DEF", "GHI");
}
除了可以依賴多重綁定的Set<Foo>唱凯,還可以依賴Provider<Set<Foo>>或者Lazy<Set<Foo>>羡忘,不可以依賴Set<Provider<Foo>>.
給每個@Provides方法添加qualifier向限定的多重綁定set貢獻元素。
@Module
class MyModuleC {
@Provides @IntoSet
@MyQualifier
static Foo provideOneFoo(DepA depA, DepB depB) {
return new Foo(depA, depB);
}
}
@Module
class MyModuleD {
@Provides
static FooSetUser provideFooSetUser(@MyQualifier Set<Foo> foos) { … }
}
Map multibindings
Dagger支持使用多重綁定向一個可注入的map貢獻entry只要這個map的key在編譯期是可見的磕昼。
在module中添加帶有返回值的方法添加@IntoMap注解和指定key的自定義注解卷雕。為了向一個限定的多重綁定map貢獻entry,需要給每個@IntoMap方法添加qualifier注解。
然后你就可以注入map(Map<K,V>)本身或包含providers的map(Map<K,Provider<V>>).當你不想一次初始化所有對象而是想一次只拿到一個值的時候或當你想在每次查詢map時都拿到新的對象時后者更有用票从。
Simple map keys
對于那些鍵是string漫雕、Class<?>或封裝的原始類型的map,使用dagger.mapkeys中定義的標準注解.
@Module
class MyModule {
@Provides @IntoMap
@StringKey("foo")
static Long provideFooValue() {
return 100L;
}
@Provides @IntoMap
@ClassKey(Thing.class)
static String provideThingValue() {
return "value for Thing";
}
}
@Component(modules = MyModule.class)
interface MyComponent {
Map<String, Long> longsByString();
Map<Class<?>, String> stringsByClass();
}
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.longsByString().get("foo")).isEqualTo(100L);
assertThat(myComponent.stringsByClass().get(Thing.class))
.isEqualTo("value for Thing");
}
對于key是枚舉或泛型化類的map,定義一個包含map的key類型成員的注解并添加@MapKey注解。
enum MyEnum {
ABC, DEF;
}
@MapKey
@interface MyEnumKey {
MyEnum value();
}
@MapKey
@interface MyNumberClassKey {
Class<? extends Number> value();
}
@Module
class MyModule {
@Provides @IntoMap
@MyEnumKey(MyEnum.ABC)
static String provideABCValue() {
return "value for ABC";
}
@Provides @IntoMap
@MyNumberClassKey(BigDecimal.class)
static String provideBigDecimalValue() {
return "value for BigDecimal";
}
}
@Component(modules = MyModule.class)
interface MyComponent {
Map<MyEnum, String> myEnumStringMap();
Map<Class<? extends Number>, String> stringsByNumberClass();
}
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.myEnumStringMap().get(MyEnum.ABC)).isEqualTo("value for ABC");
assertThat(myComponent.stringsByNumberClass.get(BigDecimal.class))
.isEqualTo("value for BigDecimal");
}
你的自定義注解可以是任何名字峰鄙,并且可以是任何正確的注解成員類型除了數(shù)組類型浸间。
Complex map keys
如果用一個注解成員不足以描述你的map的key,你可以使用整個注解作為key,只把@MapKey的unwrapValue設(shè)置為false,在這種情況下吟榴,自定義注解也可以包含數(shù)組成員魁蒜。
@MapKey(unwrapValue = false)
@interface MyKey {
String name();
Class<?> implementingClass();
int[] thresholds();
}
@Module
class MyModule {
@Provides @IntoMap
@MyKey(name = "abc", implementingClass = Abc.class, thresholds = {1, 5, 10})
static String provideAbc1510Value() {
return "foo";
}
}
@Component(modules = MyModule.class)
interface MyComponent {
Map<MyKey, String> myKeyStringMap();
}
使用@AutoAnnotation來創(chuàng)建注解實例。
如果map使用復(fù)雜key,那你可能需要在運行時創(chuàng)建@MapKey注解實例傳給get(Object)方法。最簡單的做法是使用@AutoAnnotation創(chuàng)建初始化注解實例的靜態(tài)方法兜看。更多細節(jié)查看@AutoAnnotation文檔锥咸。
class MyComponentTest {
@Test void testMyComponent() {
MyComponent myComponent = DaggerMyComponent.create();
assertThat(myComponent.myKeyStringMap()
.get(createMyKey("abc", Abc.class, new int[] {1, 5, 10}))
.isEqualTo("foo");
}
@AutoAnnotation
static MyKey createMyKey(String name, Class<?> implementingClass, int[] thresholds) {
return new AutoAnnotation_MyComponentTest_createMyKey(name, implementingClass, thresholds);
}
}
Maps whose keys are not known at compile time
編譯期map的key可以確定或可以用注解表示的Map多重綁定才有效。如果你的map不滿足這些限制细移,那就不能創(chuàng)建多重綁定map,但你可以使用set multibindings 綁定對象然后再轉(zhuǎn)換為非多重綁定的map
@Module
class MyModule {
@Provides @IntoSet
static Map.Entry<Foo, Bar> entryOne(…) {
Foo key = …;
Bar value = …;
return new SimpleImmutableEntry(key, value);
}
@Provides @IntoSet
static Map.Entry<Foo, Bar> entryTwo(…) {
Foo key = …;
Bar value = …;
return new SimpleImmutableEntry(key, value);
}
}
@Module
class MyMapModule {
@Provides
static Map<Foo, Bar> fooBarMap(Set<Map.Entry<Foo, Bar>> entries) {
Map<Foo, Bar> fooBarMap = new LinkedHashMap<>(entries.size());
for (Map.Entry<Foo, Bar> entry : entries) {
fooBarMap.put(entry.getKey(), entry.getValue());
}
return fooBarMap;
}
}
注意這種方式就不會有Map<Foo,Provider<Bar>>的自動綁定了搏予。如果你想要providers map,就需要multibound set的Map.Entry對象中包含providers弧轧。然后你的非多重綁定的map就可以有Provider值了雪侥。
@Module
class MyModule {
@Provides @IntoSet
static Map.Entry<Foo, Provider<Bar>> entry(
Provider<BarSubclass> barSubclassProvider) {
Foo key = …;
return new SimpleImmutableEntry(key, barSubclassProvider);
}
}
@Module
class MyProviderMapModule {
@Provides
static Map<Foo, Provider<Bar>> fooBarProviderMap(
Set<Map.Entry<Foo, Provider<Bar>>> entries) {
return …;
}
}
Declaring multibindings
你可以向module中返回set或map的方法添加@Multibinds注解來聲明多重綁定的set或map.
對于至少有@IntoSet,@ElementsIntoSet,或@IntoMap綁定中一個的set或map沒必要使用@Multibinds,但如果他們可能為空則必須要添加此聲明。
@Module
abstract class MyModule {
@Multibinds abstract Set<Foo> aSet();
@Multibinds @MyQualifier abstract Set<Foo> aQualifiedSet();
@Multibinds abstract Map<String, Foo> aMap();
@Multibinds @MyQualifier abstract Map<String, Foo> aQualifiedMap();
}
給定的set或map多重綁定可以被聲明多次劣针。Dagger不會實現(xiàn)或調(diào)用@Multibinds.
Alternative: @ElementsIntoSet returning an empty set
對于空的set,作為替代方案校镐,你可以在方法上添加@ElementsIntoSet.
@Module
class MyEmptySetModule {
@Provides @ElementsIntoSet
static Set<Foo> primeEmptyFooSet() {
return Collections.emptySet();
}
}
Inherited subcomponent multibindings
subcomponent中的綁定可以依賴父component中的多重綁定set或map,就像其他綁定也是可以依賴父類的一樣。subcomponent也可以向父component中綁定的多重綁定set或map添加元素捺典,只需在module中添加合適的@Provides方法鸟廓。
這種情況下,set和map根據(jù)注入的位置不同而不同襟己。當它注入到定義在subcomponent的綁定時引谜,它就會包含subcomponent和其父component的值或entry。當注入到父component中定義的綁定時擎浴,那它只包含此處定義的值或entry员咽。
@Component(modules = ParentModule.class)
interface ParentComponent {
Set<String> strings();
Map<String, String> stringMap();
ChildComponent childComponent();
}
@Module
class ParentModule {
@Provides @IntoSet
static String string1() {
"parent string 1";
}
@Provides @IntoSet
static String string2() {
"parent string 2";
}
@Provides @IntoMap
@StringKey("a")
static String stringA() {
"parent string A";
}
@Provides @IntoMap
@StringKey("b")
static String stringB() {
"parent string B";
}
}
@Subcomponent(modules = ChildModule.class)
interface ChildComponent {
Set<String> strings();
Map<String, String> stringMap();
}
@Module
class ChildModule {
@Provides @IntoSet
static String string3() {
"child string 3";
}
@Provides @IntoSet
static String string4() {
"child string 4";
}
@Provides @IntoMap
@StringKey("c")
static String stringC() {
"child string C";
}
@Provides @IntoMap
@StringKey("d")
static String stringD() {
"child string D";
}
}
@Test void testMultibindings() {
ParentComponent parentComponent = DaggerParentComponent.create();
assertThat(parentComponent.strings()).containsExactly(
"parent string 1", "parent string 2");
assertThat(parentComponent.stringMap().keySet()).containsExactly("a", "b");
ChildComponent childComponent = parentComponent.childComponent();
assertThat(childComponent.strings()).containsExactly(
"parent string 1", "parent string 2", "child string 3", "child string 4");
assertThat(childComponent.stringMap().keySet()).containsExactly(
"a", "b", "c", "d");
}
<p id="subcomponent">Subcomponents</p>
繼承和擴展父component對象圖的component稱為subcomponent。你可以使用它們把你的應(yīng)用劃分為不同子圖贮预,封裝為不同的模塊或在component中使用不同地域贝室。
subcomponent中綁定的對象可以依賴綁定在父級component中的任意對象和自己module中綁定的對象,但不能依賴兄弟級component中綁定的對象仿吞。
換句話說滑频,subcomponent的父級component的對象圖是這subcomponent對象圖的子圖。
Declaring a subcomponent
就像聲明上層component一樣唤冈,創(chuàng)建抽象類或接口并聲明抽象方法返回你需要的類型峡迷,然后添加@Subcomponent注解而不是@Component,設(shè)置@Modules.
@Subcomponent(modules = RequestModule.class)
inferface RequestComponent {
RequestHandler requestHandler();
}
Adding a subcomponent to a parent component
向父級component添加子component你虹,只需在父級component添加返回值為子component的抽象工廠方法绘搞。如果子component需要一個沒有無參構(gòu)造器的module,需要在工廠方法添加該module類型的參數(shù)。這個工廠方法可能還有其他subcomponent中的module參數(shù)傅物。(這個subcomponent會自動和parent component分享module實例)夯辖。
@Component(modules = {ServerModule.class, AuthModule.class})
interface ServerComponent {
Server server();
SessionComponent sessionComponent(SessionModule sessionModule);
}
@Subcomponent(modules = SessionModule.class)
interface SessionComponent {
SessionInfo sessionInfo();
RequestComponent requestComponent();
}
@Subcomponent(modules = {RequestModule.class, AuthModule.class})
interface RequestComponent {
RequestHandler requestHandler();
}
SessionComponent中綁定的module可以依賴ServerComponent中綁定的module,RequestComponent中綁定module同時依賴SessionComponent和ServerComponent綁定的module.
你可以通過調(diào)用parent component的工廠方法來創(chuàng)建subcomponent的實例。
ServerComponent serverComponent = DaggerServerComponent.create();
SessionComponent sessionComponent =
serverComponent.sessionComponent(new SessionModule(…));
RequestComponent requestComponent = sessionComponent.requestComponent();
通常你需要parent component中的對象綁定來創(chuàng)建subcomponent挟伙。為了完成這些楼雹,你可以基于任何component中的綁定都可以依賴這個component類型本身模孩。
class BoundInServerComponent {
@Inject ServerComponent serverComponent;
void doSomethingWithSessionInfo() {
SessionComponent sessionComponent =
serverComponent.sessionComponent(new SessionModule(…));
sessionComponent.sessionInfo().doSomething();
}
}
Subcomponent builders
你也可以按照component builders的定義方式定為component定義builder.
@Component(modules = {ServerModule.class, AuthModule.class})
interface ServerComponent {
Server server();
SessionComponent.Builder sessionComponentBuilder();
}
@Subcomponent(modules = SessionModule.class)
interface SessionComponent {
@Subcomponent.Builder
interface Builder {
Builder sessionModule(SessionModule sessionModule);
SessionComponent build();
}
}
ServerComponent serverComponent = DaggerServerComponent.create();
SessionComponent sessionComponent = serverComponent.sessionComponentBuilder()
.sessionModule(new SessionModule(…))
.build();
注入subcomponent builder
就像component本身一樣,subcomponent builder也是綁定在對象圖中的也可以被注入贮缅。所以與其注入component然后調(diào)用subcomponent builder方法不如直接注入builder榨咐。
/** 注入subcomponent builder. 這比下面的方法要簡單*/
class SessionStarterInjectingSubcomponentBuilder {
private final SessionComponent.Builder sessionComponentBuilder;
@Inject SessionStarterInjectingSubcomponentBuilder(
SessionComponent.Builder sessionComponentBuilder) {
this.sessionComponentBuilder = sessionComponentBuilder;
}
Session startSession() {
return sessionComponentBuilder
.sessionModule(new SessionModule(…))
.build()
.session();
}
}
/**
* 注入component然后調(diào)用其工廠方法. 比上面的方法麻煩 */
class SessionStarterInjectingComponent {
private final ServerComponent serverComponent;
@Inject SessionStarterInjectingComponent(ServerComponent serverComponent) {
this.serverComponent = serverComponent;
}
Session startSession() {
return serverComponent.sessionComponentBuilder()
.sessionModule(new SessionModule(…))
.build()
.session();
}
}
注意:SessionStarterInjectingSubcomponentBuilder并不依賴ServerComponent。
Subcomponents and scope
將component劃分為subcomponent的理由之一是使用scopes;在普通的沒有域的綁定中谴供,一個注入的類型可能每次拿到的是新的獨立的實例块茁。但如果這個綁定使用了域,在這個域的生命周期中所有的用戶都能拿到同一個實例桂肌。
典型的域是@Singleton数焊。使用singleton域注解綁定的用戶都拿到同一個對象。
Dagger中,可以通過@Scope注解將component和域聯(lián)系起來崎场。這種情況下佩耳,component的實現(xiàn)持有所有綁定域的對象,所以它們就可以被復(fù)用谭跨。如果Module中的@Provides方法被一個域注解了干厚,那么這個module只能設(shè)置給被同一個域注解的component。
(@Inject構(gòu)造器也可以被域注解注解螃宙。這些隱式綁定可被其他相同域的component或其后代component使用蛮瞄。被注解的實例將會綁定正確的作用域)。
subcomponent不可以與任何父級component
有相同的域谆扎,但兩個互相獨立的subcomponent可以綁定同一個作用域因為不會對哪里保存域?qū)ο笤斐善缌x挂捅。(即使使用了相同的域注解,這兩個subcomponent也擁有不同的域?qū)ο筇煤#?/p>
例如:在下面的component樹中闲先,BadChildComponent擁有和其父親RootComponent相同的@RootScpe,這是一個錯誤无蜂。但SiblingComponentOne和SiblingComponentTwo可以一起使用@ChildScope饵蒂,因為不會對兩個component中的同類型綁定造成混淆。
@RootScope @Component
interface RootComponent {
BadChildComponent badChildComponent(); // ERROR!
SiblingComponentOne siblingComponentOne();
SiblingComponentTwo siblingComponentTwo();
}
@RootScope @Subcomponent
interface BadChildComponent {…}
@ChildScope @Subcomponent
interface SiblingComponentOne {…}
@ChildScope @Subcomponent
interface SiblingComponentTwo {…}
Subcomponents for encapsulation
使用subcomponent的另一個原因是將應(yīng)用的不同部分封裝酱讶。例如:如果你的服務(wù)器中有兩個服務(wù)(或應(yīng)用中的兩個界面)共享一些綁定,如認證和授權(quán)的部分彼乌,但它們還有其他與對方?jīng)]有關(guān)系的綁定泻肯。為每個服務(wù)或界面創(chuàng)建獨立的subcomponent將共享的綁定放到parent component,這樣就說得通了。在上面的例子中慰照,F(xiàn)ooRequestComponent和 BarRequestComponent是隔離的兄弟component灶挟。你可以把他們及其module結(jié)合到一個@RequestScope component中,但會產(chǎn)生沖突的綁定。
Details
Extending multibindings
像其他的綁定一樣毒租,parent component中的multibindings對其subcomponent也是可見的稚铣。但subcomponent也可以像父component綁定的map和set添加multibinding.其他的這類貢獻只對該subcomponent和其子component的綁定可見,對其父component不可見。
@Component(modules = ParentModule.class)
interface Parent {
Map<String, Int> map();
Set<String> set();
Child child();
}
@Module
class ParentModule {
@Provides @IntoMap
@StringKey("one") static int one() {
return 1;
}
@Provides @IntoMap
@StringKey("two") static int two() {
return 2;
}
@Provides @IntoSet
static String a() {
return "a"
}
@Provides @IntoSet
static String b() {
return "b"
}
}
@Subcomponent(modules = Child.class)
interface Child {
Map<String, String> map();
Set<String> set();
}
@Module
class ChildModule {
@Provides @IntoMap
@StringKey("three") static int three() {
return 3;
}
@Provides @IntoMap
@StringKey("four") static int four() {
return 4;
}
@Provides @IntoSet
static String c() {
return "c"
}
@Provides @IntoSet
static String d() {
return "d"
}
}
Parent parent = DaggerParent.create();
Child child = parent.child();
assertThat(parent.map().keySet()).containsExactly("one", "two");
assertThat(child.map().keySet()).containsExactly("one", "two", "three", "four");
assertThat(parent.set()).containsExactly("a", "b");
assertThat(child.set()).containsExactly("a", "b", "c", "d");
Repeated modules
component和其任意一個subcomponent都設(shè)置了類型的module,那么所有這些component都會使用同一個該module實例惕医。這意味著如果一個subcomponent工廠方法包含一個重復(fù)module作為參數(shù)或者你使用重復(fù)module調(diào)用subcomponent建造方法會造成錯誤耕漱。(后者在編譯期無法檢測,是一個運行時錯誤)抬伺。
@Component(modules = {RepeatedModule.class, …})
interface ComponentOne {
ComponentTwo componentTwo(RepeatedModule repeatedModule); // COMPILE ERROR!
ComponentThree.Builder componentThreeBuilder();
}
@Subcomponent(modules = {RepeatedModule.class, …})
interface ComponentTwo { … }
@Subcomponent(modules = {RepeatedModule.class, …})
interface ComponentThree {
@Subcomponent.Builder
interface Builder {
Builder repeatedModule(RepeatedModule repeatedModule);
ComponentThree build();
}
}
DaggerComponentOne.create().componentThreeBuilder()
.repeatedModule(new RepeatedModule()) // UnsupportedOperationException!
.build();
<p id="producers">Producers</p>
Dagger Producers是一個使用Java實現(xiàn)異步依賴注入的Dagger擴展螟够。
Overview
這里假設(shè)讀者已經(jīng)熟悉Dagger2API和Guava的ListenableFuture.
Dagger Producers提供了幾種新的注解,@ProducerModule峡钓,@Producers和@ProductionComponent分別類比@Module,@Provides和@Component.我們把@ProducerModule注解的類作為producer modules,@Produces注解的方法作為producer methods,@ProductionComponent注解的接口作為producer graphs(類比于modules,provider methods,和object graphs).
并發(fā)編程是一個難題妓笙,但是一個強大而簡單的抽象可以顯著的簡化并發(fā)的編寫。出于這樣的考慮能岩,Guava 定義了 ListenableFuture接口并繼承了JDK concurrent包下的Future 接口寞宫。
所以我沒有繼續(xù)翻譯這篇文檔。詳情點擊ListenableFuture拉鹃,詳情點擊Producers辈赋。
<p id="testing">Testing</p>
使用依賴注入框架會使測試變得更簡單。本文檔中探索了一些測試使用了Dagger的應(yīng)用的策略毛俏。
Don't use Dagger for unit testing
如果你想寫一個小的單元測試測試一個@Inject注解的類炭庙,不需要在測試代碼中使用Dagger,只需調(diào)用@Inject注解的構(gòu)造器和方法并設(shè)置給@Inject注解的字段即可,也可以直接傳入模擬的依賴對象煌寇。
final class ThingDoer {
private final ThingGetter getter;
private final ThingPutter putter;
@Inject ThingDoer(ThingGetter getter, ThingPutter putter) {
this.getter = getter;
this.putter = putter;
}
String doTheThing(int howManyTimes) { /* … */ }
}
public class ThingDoerTest {
@Test
public void testDoTheThing() {
ThingDoer doer = new ThingDoer(fakeGetter, fakePutter);
assertEquals("done", doer.doTheThing(5));
}
}
Replace bindings for functional/integration/end-to-end testing
功能測試/綜合測試/端對端測試一般使用生產(chǎn)環(huán)境的應(yīng)用焕蹄,但使用fakes替換persistence,后端和驗證系統(tǒng)阀溶,讓其他部分正常運行腻脏。這種方式適用于一個或少量有限數(shù)量的測試配置替換產(chǎn)品配置中的一些綁定。
Option 1: Override bindings by subclassing modules (don’t do this!)
最簡單的方法是通過子類重寫module的@Provides方法來替換待測component中的綁定银锻。(看下面會出現(xiàn)的問題).
當創(chuàng)建Dagger component的實例時永品,你傳入需要的module實例。你可以這些module子類實例击纬,這些子類可以重寫module中的@Provides方法來替換一些綁定鼎姐。
@Component(modules = {AuthModule.class, /* … */})
interface MyApplicationComponent { /* … */ }
@Module
class AuthModule {
@Provides AuthManager authManager(AuthManagerImpl impl) {
return impl;
}
}
class FakeAuthModule extends AuthModule {
@Override
AuthManager authManager(AuthManagerImpl impl) {
return new FakeAuthManager();
}
}
MyApplicationComponent testingComponent = DaggerMyApplicationComponent.builder()
.authModule(new FakeAuthModule())
.build();
<p id="do-not-override">但這種方法也有一些局限性</p>:
- 不能改變綁定圖的靜態(tài)圖形:不能添加或移除綁定或改變綁定的依賴。具體講:
- 重寫@Provides方法不能改變其參數(shù)類型更振,縮小返回類型的范圍也不會對綁定圖造成影響炕桨。上面的例子中,testingComponent扔需要為AuthManagerImpl綁定以及其他的依賴肯腕,即使它們沒有被用到献宫。
- 同樣地,重寫的module不能添加綁定到對象圖实撒,包括multibinding(但你仍可以重寫一個SET_VALUES方法來返回一個不同的set)姊途。任何子類中新的@Provides方法都會被Dagger忽略涉瘾。這意味著虛擬的對象幾乎不能使用到依賴注入的優(yōu)勢。
- 這種方式復(fù)寫的@Provides方法不能是靜態(tài)的捷兰,所以不能省略它們的實例立叛。
<p id="separate-component-configurations">Option 2: Separate component configurations</p>
另一個方法需要對module進行更多的前期設(shè)計。應(yīng)用的每個配置(生產(chǎn)和測試)使用不同的component配置寂殉。這個測試component類型繼承了生產(chǎn)環(huán)境component并配置了不同的modules.
@Component(modules = {
OAuthModule.class, // real auth
FooServiceModule.class, // real backend
OtherApplicationModule.class,
/* … */ })
interface ProductionComponent {
Server server();
}
@Component(modules = {
FakeAuthModule.class, // fake auth
FakeFooServiceModule.class, // fake backend
OtherApplicationModule.class,
/* … */})
interface TestComponent extends ProductionComponent {
FakeAuthManager fakeAuthManager();
FakeFooService fakeFooService();
}
現(xiàn)在測試調(diào)用的主方法是DaggerTestComponent.builder()而不是DaggerProductionComponent.builder().注意此test component接口可以添加虛擬實例(fakeAuthManager()和fakeFooService())句柄這樣需要的時候就可以拿到它們控制線束囚巴。
但你會怎樣設(shè)計你的modules讓這個模式更簡單呢?
Organize modules for testability
Module類是一種工具類:是包含很多@Provides方法的集合友扰,每個@Provides方法都可以作為一個注入器提供指定類型實例彤叉。
(一個@Provides方法依賴另一個提供的類型會使幾個@Provides方法產(chǎn)生聯(lián)系,但通常它們不會明確地調(diào)用對方或依賴同一可變狀態(tài)村怪。多個@Provides方法指向同一實例字段秽浇,這樣它們就不再是獨立的了。這里的建議是將@Provides方法視為工具方法這樣測試時更易替換module)甚负。
那么怎么決定哪些@Provides方法應(yīng)該放在一個module中呢柬焕?
一種方式是將bindings分為published bindings和internal bindings,然后再決定那些published bindings有合適的選擇梭域。
Published bindings(公有綁定) 是這些向應(yīng)用的其他部分提供功能的綁定斑举。如AuthManager 或 User 或 DocDatabase 都是 published:他們都綁定在一個module中這樣應(yīng)用其他部分就可以使用他們。
剩下的綁定就是Internal(私有綁定) bindings:這類綁定在一些published 類型的實現(xiàn)中作為一部分被使用病涨。例如:OAuth client ID或OAuthKeyStore的配置綁定只會被OAuth的實現(xiàn)AuthManager使用富玷,不會被應(yīng)用的其他部分使用。這些綁定通常是package-private的或被package-private修飾既穆。
一些published 綁定會有替代選擇赎懦,特別是測試時,其他的就沒有幻工。例如:AuthManager就有可選綁定:一個測試用励两,其他適用于不同的授權(quán)/驗證協(xié)議。
但另一方面囊颅,如果AuthManager接口有一個方法返回當前在線用戶当悔,你可能想發(fā)布一個綁定提供Users,僅通過調(diào)用AuthManager的getCurrentUser()即可踢代。這個published綁定就不太可能需要替代了先鱼。
一旦你將綁定分為帶有替代選擇的published綁定、沒有替代選擇的published綁定和internal綁定奸鬓,可以這樣編排modules:
- 每個帶替代選擇的published綁定對應(yīng)一個module。(每一個替代選擇也對應(yīng)一個module掸读。)這個module僅包含一個published綁定串远,以及所有這個published 綁定需要的internal 綁定宏多。
- 所有無替代選擇的published bindings放入按照功能線組織的module中
- 公有綁定module應(yīng)該包含需要公有綁定的沒有替代選擇的module.
為每個module加上文檔描述它提供的公有綁定自然是極好的。
這是使用auth domain的例子澡罚。如果有一個AuthManager接口伸但,它可能有一個OAuth實現(xiàn)和一個測試用的模擬實現(xiàn)。綜上所述留搔,可能有一個你并不像改變配置的關(guān)于當前用戶的綁定更胖。
/**
* Provides auth bindings that will not change in different auth configurations,
* such as the current user.
*/
@Module
class AuthModule {
@Provides static User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
// Other bindings that don’t differ among AuthManager implementations.
}
/** Provides a {@link AuthManager} that uses OAuth. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class OAuthModule {
@Provides static AuthManager authManager(OAuthManager authManager) {
return authManager;
}
// Other bindings used only by OAuthManager.
}
/** Provides a fake {@link AuthManager} for testing. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class FakeAuthModule {
@Provides static AuthManager authManager(FakeAuthManager authManager) {
return authManager;
}
// Other bindings used only by FakeAuthManager.
}
然后你的正式環(huán)境配置將會使用真正的module,和測試配置使用虛擬module隔显,如上所述却妨。
<p id="project">Project Pages</p>
GitHub
Release 2.0 API(javadoc)
Developer API(javadoc)
因為官方工程是基于maven構(gòu)建的,為了便于各位Android Coder的學(xué)習(xí)括眠,我將官方工程中Android的部分拿出來放到GitHub上了彪标。
<a id="bottom">回到頂部</a>