前面已經(jīng)講了 Dagger 的基礎(chǔ)注解扣孟,并且最后我們也搭建了一個最簡單的 Dagger 注入勉耀。
這一篇我們繼續(xù)學(xué)習(xí) Dagger 更多的注解瘫想,以及如何模塊化地管理醋粟。這些將幫助我們妥善組織不同的組件靡菇、明確各自的生命周期。
@Named
依賴注入迷失
之前說過 @Module
和 @Provides
配合可以包裝沒有 @Inject
標(biāo)注的構(gòu)造函數(shù)米愿。但如果包裝了一個已經(jīng)有了 @Inject
的類會怎么樣厦凤?其實(shí)這倆有優(yōu)先級的。Dagger 會優(yōu)先從 Module 中查找實(shí)例化方法育苟,如果找不到再去找被 Inject
的標(biāo)記的構(gòu)造函數(shù)较鼓。 這也非常好理解,一般人肯定會選擇優(yōu)先去超市買東西违柏,而不是直接去拜訪工廠博烂。
一般來說,為了便于管理勇垛,我們會統(tǒng)一用 Module 封裝一層脖母,無論構(gòu)造函數(shù)有沒有被標(biāo)注。這可以幫助我們更好地管理依賴結(jié)構(gòu)與生命周期闲孤,這些后面會講到谆级。
但如果 Module 里有兩個返回值類型一樣的 Provides 呢?考慮下面的代碼:
class Stove() {
var name: String? = null
constructor(name: String) : this() {
this.name = name
}
}
@Module
class MainModule() {
@Provides
provideStove():Stove {
return Stove()
}
@Provides
provideStove():Stove { // 現(xiàn)在有兩個Provides都返回爐子
return Stove("Boom")
}
}
現(xiàn)在家樂福里有兩個爐子讼积,Dagger 不知道該買哪一個肥照,我們給這種情況起個名字叫「依賴注入迷失」。依賴注入迷失會在編譯期報錯勤众,很容易發(fā)現(xiàn)舆绎。
解決它
為了解決這個問題,必須引入一個新的注解 @Named
们颜,也就是廚師會指明到底需要哪個型號的爐子吕朵,這樣就不會買錯了。同時窥突,記得給超時貨架上的爐子也表明型號努溃,不然怎么買對吧 -。-
改造后的 Module 與 Chef 如下:
@Module
class MainModule() {
@Provides
@Named("noname")
provideStove():Stove {
return Stove()
}
@Provides
@Named("boom")
provideStove():Stove { // 現(xiàn)在有兩個Provides都返回爐子
return Stove("Boom")
}
}
class Chef() {
@Inject
@Named("noname")
val stove1: Stove
@Inject
@Named("boom")
val stove2: Stove
}
我們的廚師比較貪婪阻问,他兩個型號全都要梧税。但與一開始胡亂買不同,現(xiàn)在他清楚地指明了我需要兩個型號,并且能分清這兩個型號第队。于是就不會報錯了哮塞。
@Qualifier
Qualifier
與 Named
的作用一模一樣。只不過 Named 是用單純的字符串區(qū)分凳谦,而 Qualifier 需要先自定義注解∫涑現(xiàn)在我們把剛才的例子改用 Qualifier 實(shí)現(xiàn)。
// 定義一個新的注解尸执,名叫 StoveQualifier
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class StoveQualifier
@Module
class MainModule() {
@Provides
@StoveQualifier("noname")
provideStove():Stove {
return Stove()
}
@Provides
@StoveQualifier("boom")
provideStove():Stove { // 現(xiàn)在有兩個Provides都返回爐子
return Stove("Boom")
}
}
class Chef() {
@Inject
@StoveQualifier("noname")
val stove1: Stove
@Inject
@StoveQualifier("boom")
val stove2: Stove
}
看到?jīng)]邻眷,和 Named 用法一模一樣對吧。肯定有人要問剔交,既然那么麻煩問什么不直接用 Named 呢。
你可以把 Qualifier 看做是自定義命名空間改衩。之前所有的型號都標(biāo)注在 Named 空間下岖常。也就是空調(diào)、爐子葫督、電磁爐竭鞍、冰箱等等构眯,型號全部糅雜在一起伍掀,顯然這不是個好辦法。通過自定義 Qualifier入录,我們可以讓每個類有自己的型號命名空間洽胶,不要擔(dān)心沖突與混淆了晒夹。
模塊化管理
一開始已經(jīng)提到,為了便于管理我們會統(tǒng)一用 Module 封裝一層姊氓,而 Module 最終要被關(guān)聯(lián)到 Component丐怯。因此問題的關(guān)鍵就成了該如何組織 Component。
劃分原則
既然標(biāo)題叫 Dagger2 in Android翔横,自然是要重點(diǎn)考慮 Android 上面的應(yīng)用读跷。一個思維正常的程序猿都不會把所有注入都寫進(jìn)一個 Component,否則會變得非常龐大禾唁、難以維護(hù)效览。但是劃分的粒度也不可以太小,如果為每個類都創(chuàng)建一個 Component荡短,也會變得非常復(fù)雜丐枉、難以維護(hù)。
讓我們回到一開始 Dagger 到底是干什么用的肢预?經(jīng)過一輪學(xué)習(xí)相信大家都有自己的答案矛洞。我認(rèn)為它主要作用是「創(chuàng)建并管理對象,將其注入到需要它們的類」。既然是管理對象沼本,那就不得不考慮生命周期噩峦。因此基于生命周期的劃分也許是個不錯的點(diǎn)子。
一個 Android 應(yīng)用有很多生命周期抽兆,大致有兩類:
- Application:這是最長的生命周期识补,從我們應(yīng)用啟動開始,直到被徹底銷毀辫红。
- Activity/Fragment: 都表示一個頁面凭涂。打開時開始,離開時銷毀贴妻。
所以我們完全可以按照生命周期來對 Component 進(jìn)行劃分切油。
組織 Component
我們知道 Component 本質(zhì)就是一個接口(抽象類),因此它互相也可以有聯(lián)系名惩,關(guān)系分為兩種:依賴關(guān)系與包含關(guān)系澎胡。
依賴關(guān)系(組件依賴)
現(xiàn)在我們有兩個 Component,分別是 AppComponent 與 ActivityComponent娩鹉,前者持有一個全局 Context 對象攻谁,我們希望后者依賴前者。那么可以這么做:
@Module
class AppModule(private val context: Context) {
@Provides
fun provideContext(): Context = context
}
@Component(modules = [AppModule::class])
interface AppComponent {
fun context(): Context // 注意這行
}
@Module
class ActivityModule {
@Provides
fun provideSp(context: Context) =
context.getSharedPreferences("Cooker", Context.MODE_PRIVATE)
}
// 聲明了依賴關(guān)系
@Component(dependencies = [AppComponent::class], modules = [ActivityModule::class])
interface ActivityComponent {
}
分析一下這段代碼:
ActivityModule 定義了一個 Provides 能夠返回 SharedPreferences 的實(shí)例弯予。但是創(chuàng)建這個實(shí)例需要 context戚宦,它是哪來的?由于它聲明了依賴 AppComponent锈嫩,而 AppComponent 擁有的 AppModule 中有可以提供 context 的 Provides受楼,因此 ActivityModule 從 AppComponent 那里拿到了 context。
但這不是無條件的祠挫,依賴別人的前提是別人愿意被你依賴才行那槽。因此 AppComponent 中必須顯示地定義一個能夠返回 Context 類型的函數(shù),依賴它的 Component 才能拿到等舔。如果不定義骚灸,即使有,也不會給別人的慌植。
注意區(qū)分 Component 中的函數(shù)與 Module 中 Provides 的區(qū)別:前者作用是:① 用于注入 ② 用于給依賴的 Component 提供對象甚牲;后者作用僅僅是創(chuàng)建對象。
包含關(guān)系(子組件)(組件繼承)
依賴就像朋友蝶柿,對方愿意才可以分享丈钙。包含就像父母,分享是無條件的交汤。
聲明繼承需要以下幾步
- 子 Component 用
@Subcomponent
注解雏赦。 - 子 Component 聲明一個 Builder 來告訴父 Component 如何創(chuàng)建自己劫笙。
- 父 Component 對應(yīng)的 Module 用
subcomponents
屬性來指明擁有哪些子 Component。 - 父 Component 聲明一個抽象方法來獲取子 Component 的 Builder星岗。
上面的例子用包含關(guān)系可以這樣改寫:
@SubComponent(modules = [ActivityModule::class]) // 子Component用@Subcomponent注解填大。
interface ActivityComponent {
// 聲明一個Builder來告訴父Component如何創(chuàng)建自己
@Subcomponent.Builder
interface Builder {
fun build(): ActivityComponent
}
}
// 父Component對應(yīng)的Module用subcomponents屬性來指明擁有哪些子Component
@Module(subcomponents = [ActivityComponent::class])
class AppModule(private val context: Context) {
@Provides
fun provideContext(): Context = context
}
@Component(modules = [AppModule::class])
interface AppComponent {
//fun context(): Context // 不需要顯示定義了
// 父Component聲明一個抽象方法來獲取子Component的Builder
fun activityComponent(): ActivityComponent.Builder
}
聲明包含關(guān)系后,父接口所能提供的所有對象子接口下的 Module 都可以直接使用俏橘,不再需要顯示聲明了允华。
對于包含關(guān)系,子 Component 將不再生成 DaggerXxxComponent 類寥掐,需要通過父 Component 的實(shí)例來創(chuàng)建子 Component靴寂。
對比
相同點(diǎn):
- 都可以使用父接口所提供的對象。
不同點(diǎn):
- 生成代碼不同召耘。依賴關(guān)系每一個 Component 都會生成一個 DaggerXxxComponent 類百炬;而包含關(guān)系只會生成一個。
- 對父接口對象訪問限制不同污它。依賴關(guān)系必須主動聲明才能獲取到收壕;包含關(guān)系默認(rèn)能獲取到。
那么究竟選用哪個轨蛤,似乎沒有準(zhǔn)確的規(guī)范,在更多的實(shí)踐中體會吧虫埂。(一般在 Android 中祥山,會讓 Activity 包含于 AppComponent)
總結(jié)
這一章主要學(xué)習(xí)了 Dagger 的模塊化管理。一開始提到過掉伏,Dagger 還可以管理對象的生命周期缝呕,這是一個非常重要也是一個非常容易弄錯的方面,我們將在下一章單獨(dú)討論斧散。
有了上一章的鋪墊供常,本章類比不是特別多了,如果有概念忘記的(特別在講模塊化的時候)一定要回到上一章看看鸡捐,不然下一章一定會更加痛苦栈暇。