Dagger2 in Android(二)進(jìn)階

前面已經(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

QualifierNamed 的作用一模一樣。只不過 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)系(子組件)(組件繼承)

依賴就像朋友蝶柿,對方愿意才可以分享丈钙。包含就像父母,分享是無條件的交汤。

聲明繼承需要以下幾步

  1. 子 Component 用 @Subcomponent 注解雏赦。
  2. 子 Component 聲明一個 Builder 來告訴父 Component 如何創(chuàng)建自己劫笙。
  3. 父 Component 對應(yīng)的 Module 用 subcomponents 屬性來指明擁有哪些子 Component。
  4. 父 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ú)討論斧散。

有了上一章的鋪墊供常,本章類比不是特別多了,如果有概念忘記的(特別在講模塊化的時候)一定要回到上一章看看鸡捐,不然下一章一定會更加痛苦栈暇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市箍镜,隨后出現(xiàn)的幾起案子源祈,更是在濱河造成了極大的恐慌,老刑警劉巖色迂,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件香缺,死亡現(xiàn)場離奇詭異,居然都是意外死亡歇僧,警方通過查閱死者的電腦和手機(jī)图张,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人祸轮,你說我怎么就攤上這事兽埃。” “怎么了倔撞?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵讲仰,是天一觀的道長。 經(jīng)常有香客問我痪蝇,道長鄙陡,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任躏啰,我火速辦了婚禮趁矾,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘给僵。我一直安慰自己毫捣,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布帝际。 她就那樣靜靜地躺著蔓同,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蹲诀。 梳的紋絲不亂的頭發(fā)上斑粱,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音脯爪,去河邊找鬼则北。 笑死,一個胖子當(dāng)著我的面吹牛痕慢,可吹牛的內(nèi)容都是我干的尚揣。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掖举,長吁一口氣:“原來是場噩夢啊……” “哼快骗!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起塔次,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤滨巴,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后俺叭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體恭取,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年熄守,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜈垮。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片耗跛。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖攒发,靈堂內(nèi)的尸體忽然破棺而出调塌,到底是詐尸還是另有隱情,我是刑警寧澤惠猿,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布羔砾,位于F島的核電站,受9級特大地震影響偶妖,放射性物質(zhì)發(fā)生泄漏姜凄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一趾访、第九天 我趴在偏房一處隱蔽的房頂上張望态秧。 院中可真熱鬧,春花似錦扼鞋、人聲如沸申鱼。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽捐友。三九已至,卻和暖如春溃槐,著一層夾襖步出監(jiān)牢的瞬間楚殿,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工竿痰, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人砌溺。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓影涉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親规伐。 傳聞我的和親對象是個殘疾皇子蟹倾,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容