主頁 > 軟體設計 > Jetpack新成員,一篇文章帶你玩轉Hilt和依賴注入

Jetpack新成員,一篇文章帶你玩轉Hilt和依賴注入

2020-11-25 11:45:15 軟體設計

本文同步發表于我的微信公眾號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每個作業日都有文章更新,

各位小伙伴們大家早上好,

終于要寫這樣一篇我自己都比較怕的文章了,

雖然今年的Google I/O大會由于疫情的原因沒能開成,但是Google每年要發布的各種新技術可一樣都沒少,

隨著Android 11系統的發布,Jetpack家族又迎來了不少新成員,包括Hilt、App Startup、Paging3等等,

關于App Startup,我在之前已經寫過一篇文章進行講解了,感興趣的朋友可以參考 Jetpack新成員,App Startup一篇就懂 這篇文章 ,

本篇文章的主題是Hilt,

Hilt是一個功能強大且用法簡單的依賴注入框架,同時也可以說是今年Jetpack家族中最重要的一名新成員,

那么為什么說這是一篇我自己都比較怕的文章呢?因為關于依賴注入的文章太難寫了,我覺得如果只是向大家講解Hilt的用法倒還算是簡單,但是如果想要讓大家弄明白為什么要使用Hilt?或者再進一步,為什么要使用依賴注入?這就不是一個非常好寫的話題了,

本篇文章我會嘗試將以上幾個問題全部講清楚,希望我可以做得到,

另外請注意,依賴注入這個話題本身是不分語言的,但由于我還要在本文中講解Hilt的知識,所以文中所有的代碼都會使用Kotlin來演示,對Kotlin還不熟悉的朋友,可以去參考我的新書 《第一行代碼 Android 第3版》 ,


為什么要使用依賴注入?

依賴注入的英文名是Dependency Injection,簡稱DI,事實上這并不是什么新興的名詞,而是軟體工程學當中比較古老的概念了,

如果要說對于依賴注入最知名的應用,大概就是Java中的Spring框架了,Spring在剛開始其實就是一個用于處理依賴注入的框架,后來才慢慢變成了一個功能更加廣泛的綜合型框架,

我在學生時代學習Spring時產生了和絕大多數開發者一樣的疑惑,就是為什么我們要使用依賴注入呢?

現在的我或許可以給出更好的答案了,一言以蔽之:解耦,

耦合度過高可能會是你的專案中一個比較嚴重的隱患,它會讓你的專案到了后期變得越來越難以維護,

為了讓大家更容易理解,這里我準備通過一個具體的例子來講述一下,

假設我們開了一家卡車配送公司,公司里目前有一輛卡車每天用來送貨,并以此賺錢維持公司運營,

今天接到了一個配送訂單,有客戶委托我們公司去配送兩臺電腦,

為了完成這個任務,我們可以撰寫出如下代碼:

class Truck {

    val computer1 = Computer()
    val computer2 = Computer()

    fun deliver() {
        loadToTruck(computer1)
        loadToTruck(computer2)
        beginToDeliver()
    }

}

這里有一輛卡車Truck,卡車中有一個deliver()函式用于執行配送任務,我們在deliver()函式中先將兩臺電腦裝上卡車,然后開始進行配送,

這種寫法可以完成任務嗎?當然可以,我們的任務是配送兩臺電腦,現在將兩臺電腦都配送出去了,任務當然也就完成了,

但是這種寫法有沒有問題呢?有,而且很嚴重,

具體問題在哪里呢?明眼的小伙伴應該已經看出來了,我們在Truck類當中創建了兩臺電腦的實體,然后才對它們進行的配送,也就是說,現在我們的卡車不光要會送貨,還要會生產電腦才行,

這就是剛才所說的耦合度過高所造成的問題,卡車和電腦這兩樣原本不相干的東西耦合到一起去了,

如果你覺得目前這種寫法問題還不算嚴重,第二天公司又接到了一個新的訂單,要求我們去配送手機,因此這輛卡車還要會生產手機才行,第三天又接到了一個配送蔬果的訂單,那么這輛卡車還要會種地,,,

最后你會發現,這已經不是一輛卡車了,而是一個全球商品制造中心,

現在我們都意識到了問題的嚴重性,那么回過頭來反思一下,我們的專案到底是從哪里開始跑偏的呢?

這就是一個結構設計上的問題了,仔細思考一下,卡車其實并不需要關心配送的貨物具體是什么,它的任務就只是負責送貨而已,因此你可以理解成,卡車是依賴于貨物的,給了卡車貨物,它就去送貨,不給卡車貨物,它就待命,

那么根據這種說法,我們就可以將剛才的代碼進行如下修改:

class Truck {

    lateinit var cargos: List<Cargo>

    fun deliver() {
        for (cargo in cargos) {
            loadToTruck(cargo)
        }
        beginToDeliver()
    }

}

現在Truck類當中添加了cargos欄位,這就意味著,卡車是依賴于貨物的了,經過這樣的修改之后,我們的卡車不再關心任何商品制造的事情,而是依賴了什么貨物,就去配送什么貨物,只做本職應該做的事情,

這種寫法,我們就可以稱之為:依賴注入,


依賴注入框架的作用是什么?

目前Truck類已經設計得比較合理了,但是緊接著又會產生一個新的問題,假如我們的身份現在發生了變化,變成了一家電腦公司的老板,我該如何讓一輛卡車來幫我運送電腦呢?

這還不好辦?很多人自然而然就能寫出如下代碼:

class ComputerCompany {

    val computer1 = Computer()
    val computer2 = Computer()

    fun deliverByTruck() {
        val truck = Truck()
        truck.cargos = listOf(computer1, computer2)
        truck.deliver()
    }

}

這段代碼同樣是可以正常作業的,但是這段代碼同樣也存在比較嚴重的問題,

問題在哪兒呢?就是在deliverByTruck()函式中,為了讓卡車幫我們送貨,這里自己制造了一輛卡車,這很明顯是不合理的,電腦公司應該只負責生產電腦,它不應該去生產卡車,

因此,更加合理的做法是,我們通過撥打卡車配送公司的電話,讓他們派輛空閑的卡車過來,這樣就不用自己去造車了,當卡車到達之后,我們再將電腦裝上卡車,然后執行配送任務即可,

這個程序可以用如下示意圖來表示:

使用這種結構設計出來的專案,將會擁有非常出色的擴展性,假如現在又有一家蔬果公司需要找一輛卡車來送菜,我們完全可以使用同樣的結構來完成任務:

注意,重點的地方來了,呼叫卡車公司并讓他們安排空閑車輛的這個部分,我們可以通過自己手寫來實作,也可以借助一些依賴注入框架來簡化這個程序,

因此,如果你想問依賴注入框架的作用是什么,那么實際上它就是為了替換下圖所示的部分,

看到這里,希望你已經能明白為什么我們要使用依賴注入,以及依賴注入框架的作用是什么了,


Android開發也需要依賴注入框架嗎?

有不少人會存在這樣的觀點,他們認為依賴注入框架主要是應用在服務器這用復雜度比較高的程式上的,Android開發通常根本就用不到依賴注入框架,

這種觀點在我看來可能并沒有錯,不過我更希望大家把依賴注入框架當成是一個幫助我們簡化代碼和優化專案的工具,而不是一個額外的負擔,

所以,不管程式的復雜度是高是低,既然依賴注入框架可以幫助我們簡化代碼和優化專案,那么就完全可以使用它,

說到優化專案,大家可能覺得我剛才舉的讓卡車去生產電腦的例子太搞笑了,可是你信不信,在我們實際的開發程序中,這樣的例子簡直每天都在上演,

思考一下,你平時在Activity中撰寫的代碼,有沒有創建過其實并不應該由Activity去創建的實體呢?

比如說我們都會使用OkHttp來進行網路請求,你有沒有在Activity中創建過OkHttpClient的實體呢?如果有的話,那么恭喜你,你相當于就是在讓卡車去生產電腦了(Activity是卡車,OkHttpClient是電腦),

當然,如果只是一個比較簡單的專案,我們確實可以在Activity中去創建OkHttpClient的實體,不考慮代碼耦合度的話,即使真的讓卡車去生產電腦,也不會出現什么太大的問題,因為它的確可以正常作業,至少暫時可以,

我第一次清晰地意識到自己迫切需要一個依賴注入框架,是我在使用MVVM架構來搭建專案的時候,

在Android開發者官網有一張關于MVVM架構的示意圖,如下圖所示,

這就是現在Google最推薦我們使用的Android應用程式架構,

為防止有些同學還沒接觸過MVVM,我來對這張圖做一下簡單的解釋,

這張架構圖告訴我們,一個擁有良好架構的專案應該要分為若干層,

其中綠色部分表示的是UI控制層,這部分就是我們平時寫的Activity和Fragment,

藍色部分表示的是ViewModel層,ViewModel用于持有和UI元素相關的資料,以及負責和倉庫之間進行通訊,

橙色部分表示的是倉庫層,倉庫層要做的作業是判斷介面請求的資料應該是從資料庫中讀取還是從網路中獲取,并將資料回傳給呼叫方,簡而言之,倉庫的作業就是在本地和網路資料之間做一個分配和調度的作業,

另外,圖中所有的箭頭都是單向的,比方說Activity指向了ViewModel,表示Activity是依賴于ViewModel的,但是反過來ViewModel不能依賴于Activity,其他的幾層也是一樣的道理,一個箭頭就表示一個依賴關系,

還有,依賴關系是不可以跨層的,比方說UI控制層不能和倉庫層有依賴關系,每一層的組件都只能和它的相鄰層互動,

使用這套架構設計出來的專案,結構清晰、分層明確,一定會是一個代碼質量非常高的專案,

但是在按照這張架構示意圖具體實作的程序中,我卻發現了一個問題,

UI控制層當中,Activity是四大組件之一,它的實體創建是不用我們去操心的,

而ViewModel層當中,Google在Jetpack中提供了專門的API來獲取ViewModel的實體,所以它的實體創建也是不用我們去操心的,

但是到了倉庫層,一個尷尬的事情出現了,誰應該去負責創建倉庫的實體呢?ViewModel嗎?不對,ViewModel只是依賴了倉庫而已,它不應該負責創建倉庫的實體,并且其他不同的ViewModel也可能會依賴同一個倉庫實體,Activity嗎?這就更扯了,因為Activity和ViewModel通常都是一一對應的,

所以最后我發現,沒人應該負責創建倉庫的實體,最簡單的方式就是將倉庫設定成單例類,這樣就不需要操心實體創建的問題了,

但是設定成單例類之后又會出現一個新的問題,就是依賴關系不可以跨層這個規則被打破了,因為倉庫已經設定成了單例類,那么自然相當于誰都擁有它的依賴關系了,UI控制層可以繞過ViewModel層,直接和倉庫層進行通訊,

從代碼設計的層面來講,這是一個非常不好解決的問題,但如果我們借助依賴注入框架,就可以很靈活地解決這個問題,

從剛才的示意圖中已經可以看出,依賴注入框架就是幫助我們呼叫和安排空閑卡車的,我并不關心這個卡車是怎么來的,只要你能幫我送貨就行,

因此,ViewModel層也不應該關心倉庫的實體是怎么來的,我只需要宣告ViewModel是需要依賴倉庫的,剩下的讓依賴注入框架幫我去解決就行了,

通過這樣一個類比,你是不是對于依賴注入框架的理解又更加深刻了一點呢?


Android常用的依賴注入框架

接下來我們聊一聊Android有哪些常用的依賴注入框架,

在很早的時候,絕大部分的Android開發者都是沒有使用依賴注入框架這種意識的,

大名鼎鼎的Square公司在2012年推出了至今仍然知名度極高的開源依賴注入框架:Dagger,

Square公司有許多非常成功的開源專案,OkHttp、Retrofit、LeakCanary等等大家都耳熟能詳,而且幾乎所有的Android專案都在使用,但是Dagger卻空有知名度,現在應該沒有任何專案還在使用它了,為什么呢?

這就是一個很有意思的故事了,

Dagger的依賴注入理念雖然非常先進,但是卻存在一個問題,它是基于Java反射去實作的,這就導致了兩個潛在的隱患,

第一,我們都知道反射是比較耗時的,所以用這種方式會降低程式的運行效率,當然這個問題并不大,因為現在的程式中到處都在用反射,

第二,依賴注入框架的用法總體來說是非常有難度的,除非你能相當熟練地使用它,否則很難一次性撰寫正確,而基于反射實作的依賴注入功能,使得在編譯期我們無法得知依賴注入的用法到底對不對,只能在運行時通程序式有沒有崩潰來判斷,這樣測驗的效率就很低,而且容易將一些bug隱藏得很深,

接下來就到了最有意思的地方,我們現在都知道Dagger的實作方式存在問題,那么Dagger2自然是要去解決這些問題的,但是Dagger2并不是由Square開發的,而是由Google開發的,

這就很奇怪了,正常情況下一個庫的1版和2版應該都是由同一個公司或者同一批開發者維護的,怎么Dagger1到Dagger2會變化這么大呢?我也不知道為什么,但是我注意到,Google現在維護的Dagger專案是從Square的Dagger專案Fork過來的,

所以我猜測,大概是Google Fork了一份Dagger的原始碼,然后在此基礎上進行修改,并發布了Dagger2版本,Square看到了之后,認為Google的這個版本做得非常好,自己沒有必要再重做一遍,也沒有必要繼續維護Dagger1了,所以就發布了這樣一條宣告:

那么Dagger2和Dagger1不同的地方在哪里呢?最重要的不同點在于,實作方式完全發生了變化,剛才我們已經知道,Dagger1是基于Java反射實作的,并且列舉了它的一些弊端,而Google開發的Dagger2是基于Java注解實作的,這樣就把反射的那些弊端全部解決了,

通過注解,Dagger2會在編譯時期自動生成用于依賴注入的代碼,所以不會增加任何運行耗時,另外,Dagger2會在編譯時期檢查開發者的依賴注入用法是否正確,如果不正確的話則會直接編譯失敗,這樣就能將問題盡可能早地拋出,也就是說,只要你的專案正常編譯通過,基本也就說明你的依賴注入用法沒什么問題了,

那么Google的這個Dagger2有沒有取得成功呢?簡直可以說是大獲成功,

根據Google官方給出的資料,在Google Play排名前1000的App當中,有74%的App都使用了Dagger2,

這里我要提一句,海外和國內的Android開發者喜歡研究的技術堆疊不太一樣,在海外,沒有人去研究像熱修復或插件化這種國內特有的Android技術,那么你可能想問了,海外開發者們都是學什么進階的呢?

答案就是Dagger2,

是的,Dagger2在海外是非常受到歡迎和廣泛認可的技術堆疊,如果你能用得一手好Dagger2,基本也就說明你是水平比較高的開發者了,

不過有趣的是,在國內反倒沒有多少人愿意去使用Dagger2,我在公眾號之前也推送過幾篇關于Dagger2的文章,但是從反饋上來看感覺這項技術在國內始終比較小眾,

雖然Dagger2在海外很受歡迎,但是其復雜程度也是眾所周知的,如果你不能很好地使用它的話,反而可能會拖累你的專案,所以一直也有聲音說,使用Dagger2會將一些簡單的專案過度設計,

根據Android團隊發布的調查,49%的Android開發者希望Jetpack中能夠提供一個更加簡單的依賴注入解決方案,

于是,Google在今年發布了Hilt,

你是不是覺得我講了這么多的長篇大論,現在才終于講到主題?不要這么想,我認為了解以上這些綜合的內容,比僅僅只是掌握了Hilt的用法要更加重要,

我們都知道,Dagger是匕首的意思,依賴注入就好像是把匕首直接插入了需要注入的地方,直擊要害,

而Hilt是刀把的意思,它把匕首最鋒利的地方隱藏了起來,因為如果你用不好匕首的話反而可能會誤傷自己,Hilt給你提供了一個安穩的把手,確保你可以安全簡單地使用,

事實上,Hilt和Dagger2有著千絲萬縷的關系,Hilt就是Android團隊聯系了Dagger2團隊,一起開發出來的一個專門面向Android的依賴注入框架,相比于Dagger2,Hilt最明顯的特征就是:1. 簡單,2. 提供了Android專屬的API,

那么接下來,就讓我們開始學習一下Hilt的具體用法,


引入Hilt

在開始使用Hilt之前,我們需要先將Hilt引入到你當前的專案當中,這個程序稍微有點繁瑣,所以請大家一步步按照文章中的步驟操作,

第一步,我們需要在專案根目錄的build.gradle檔案中配置Hilt的插件路徑:

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

可以看到,目前Hilt最新的插件版本還在alpha階段,但是沒有關系,我自己用下來感覺已經是相當穩定了,等正式版本發布之后升級一下就可以了,用法上不會有什么太大變化,

接下來,在app/build.gradle檔案中,引入Hilt的插件并添加Hilt的依賴庫:

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

這里同時還引入了kotlin-kapt插件,是因為Hilt是基于編譯時注解來實作的,而啟用編譯時注解功能一定要先添加kotlin-kapt插件,如果你還在用Java開發專案,則可以不引入這個插件,同時將添加注解依賴庫時使用的kapt關鍵字改成annotationProcessor即可,

最后,由于Hilt還會用到Java 8的特性,所以我們還得在當前專案中啟用Java 8的功能,編輯app/build.gradle檔案,并添加如下內容即可:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

好了,要配置的內容總共就這么多,現在你已經成功將Hilt引入到了你的專案當中,下面我們就來學習一下如何使用它吧,


Hilt的簡單用法

我們先從最簡單的功能學起,

相信大家都知道,每個Android程式中都會有一個Application,這個Application可以自定義,也可以不定義,如果你不定義的話,系統會使用一個默認的Application,

而到了Hilt當中,你必須要自定義一個Application才行,否則Hilt將無法正常作業,

這里我們自定義一個MyApplication類,代碼如下所示:

@HiltAndroidApp
class MyApplication : Application() {
}

你的自定義Application中可以不寫任何代碼,但是必須要加上一個@HiltAndroidApp注解,這是使用Hilt的一個必備前提,

接下來將MyApplication注冊到你的AndroidManifest.xml檔案當中:

<application
    android:name=".MyApplication"
    ...>
    
</application>

這樣準備作業就算是完成了,接下來的作業就是根據你具體的業務邏輯使用Hilt去進行依賴注入,

Hilt大幅簡化了Dagger2的用法,使得我們不用通過@Component注解去撰寫橋接層的邏輯,但是也因此限定了注入功能只能從幾個Android固定的入口點開始,

Hilt一共支持6個入口點,分別是:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

其中,只有Application這個入口點是使用@HiltAndroidApp注解來宣告的,這個我們剛才已經看過了,其他的所有入口點,都是用@AndroidEntryPoint注解來宣告的,

以最常見的Activity來舉例吧,如果我希望在Activity中進行依賴注入,那么只需要這樣宣告Activity即可:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
    
}

接下來我們嘗試向Activity中注入點東西吧,注入什么呢?還記得剛才的那輛卡車嗎,我們試著看把它注入到Activity當中吧,

定義一個Truck類,代碼如下所示:

class Truck {

    fun deliver() {
        println("Truck is delivering cargo.")
    }

}

可以看到,目前這輛卡車有一個deliver()方法,說明它具備送貨功能,

然后修改Activity中的代碼,如下所示:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var truck: Truck

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        truck.deliver()
    }

}

這里的代碼可能乍一看上去稍微有點奇怪,我來解釋一下,

首先lateinit是Kotlin中的關鍵字,和Hilt無關,這個關鍵字用于對變數延遲初始化,因為Kotlin默認在宣告一個變數時就要對其進行初始化,而這里我們并不想手動初始化,所以要加上lateinit,如果你是用Java開發的話,那么可以無視這個關鍵字,

接下來我們在truck欄位的上方宣告了一個@Inject注解,表示我希望通過Hilt來注入truck這個欄位,如果讓我類比的話,這大概就相當于電腦公司打電話讓卡車配送公司安排卡車的程序,我們可以把MainActivity看作電腦公司,它是依賴于卡車的,但是至于這個卡車是怎么來的,電腦公司并不關心,而Hilt在這里承擔的職責就類似于卡車配送公司,它負責想辦法安排車輛,甚至有義務造一輛出來,

另外提一句,Hilt注入的欄位是不可以宣告成private的,這里大家一定要注意,

不過代碼寫到這里還是不可以正常作業的,因為Hilt并不知道該如何提供一輛卡車,因此,我們還需要對Truck類進行如下修改:

class Truck @Inject constructor() {

    fun deliver() {
        println("Truck is delivering cargo.")
    }

}

這里我們在Truck類的建構式上宣告了一個@Inject注解,其實就是在告訴Hilt,你是可以通過這個建構式來安排一輛卡車的,

好了,就是這么簡單,現在可以運行一下程式了,你將會在Logcat中看到如下內容:

說明卡車真的已經在好好送貨了,

有沒有覺得很神奇?我們在MainActivity中并沒有去創建Truck的實體,只是用@Inject宣告了一下,結果真的可以呼叫它的deliver()方法,

這就是Hilt給我們提供的依賴注入功能,


帶引數的依賴注入

必須承認,剛才我們所舉的例子確實太簡單了,在真實的編程場景中用處應該非常有限,因為真實場景中不可能永遠是這樣的理想情況,

那么下面我們就開始逐步學習如何在各種更加復雜的場景下使用Hilt進行依賴注入,

首先一個很容易想到的場景,如果我的建構式中帶有引數,Hilt要如何進行依賴注入呢?

我們對Truck類進行如下改造:

class Truck @Inject constructor(val driver: Driver) {

    fun deliver() {
        println("Truck is delivering cargo. Driven by $driver")
    }

}

可以看到,現在Truck類的建構式中增加了一個Driver引數,說明卡車是依賴一位司機的,畢竟沒有司機的話卡車自己是不會開的,

那么問題來了,既然卡車是依賴司機的,Hilt現在要如何對卡車進行依賴注入呢?畢竟Hilt不知道這位司機來自何處,

這個問題其實沒有想象中的困難,因為既然卡車是依賴司機的,那么如果我們想要對卡車進行依賴注入,自然首先要能對司機進行依賴注入才行,

所以可以這樣去宣告Driver類:

class Driver @Inject constructor() {
}

非常簡單,我們在Driver類的建構式上宣告了一個@Inject注解,如此一來,Driver類就變成了無參建構式的依賴注入方式,

然后就不需要再修改任何代碼了,因為Hilt既然知道了要如何依賴注入Driver,也就知道要如何依賴注入Truck了,

總結一下,就是Truck的建構式中所依賴的所有其他物件都支持依賴注入了,那么Truck才可以被依賴注入,

現在重新運行一下程式,列印日志如下所示:

可以看到,現在卡車正在被一位司機駕駛,這位司機的身份證號是de5edf5,


介面的依賴注入

解決了帶參建構式的依賴注入,接下來我們繼續看更加復雜的場景:如何對介面進行依賴注入,

毫無疑問,我們目前所掌握的技術是無法對介面進行依賴注入的,原因也很簡單,介面沒有建構式,

不過不用擔心,Hilt對介面的依賴注入提供了相當完善的支持,所以你很快就能掌握這項技能,

我們繼續通過具體的示例來學習,

任何一輛卡車都需要有引擎才可以正常行駛,那么這里我定義一個Engine介面,如下所示:

interface Engine {
    fun start()
    fun shutdown()
}

非常簡單,介面中有兩個待實作方法,分別用于啟用引擎和關閉引擎,

既然有介面,那就還要有實作類才行,這里我再定義一個GasEngine類,并實作Engine介面,代碼如下所示:

class GasEngine() : Engine {
    override fun start() {
        println("Gas engine start.")
    }

    override fun shutdown() {
        println("Gas engine shutdown.")
    }
}

可以看到,我們在GasEngine中實作了啟動引擎和關閉引擎的功能,

另外,現在新能源汽車非常火,特斯拉已經快要遍地都是了,所以汽車引擎除了傳統的燃油引擎之外,現在還有了電動引擎,于是這里我們再定義一個ElectricEngine類,并實作Engine介面,代碼如下所示:

class ElectricEngine() : Engine {
    override fun start() {
        println("Electric engine start.")
    }

    override fun shutdown() {
        println("Electric engine shutdown.")
    }
}

類似地,ElectricEngine中也實作了啟動引擎和關閉引擎的功能,

剛才已經說了,任何一輛卡車都需要有引擎才可以正常行駛,也就是說,卡車是依賴于引擎的,現在我想要通過依賴注入的方式,將引擎注入到卡車當中,那么需要怎么寫呢?

根據剛才已學到的知識,最直觀的寫法就是這樣:

class Truck @Inject constructor(val driver: Driver) {

    @Inject
    lateinit var engine: Engine
    ...

}

我們在Truck中宣告一個engine欄位,這就說明Truck是依賴于Engine的了,然后在engine欄位的上方使用@Inject注解對該欄位進行注入,或者你也可以將engine欄位宣告到建構式當中,這樣就不需要加入@Inject注解了,效果是一樣的,

假如Engine欄位是一個普通的類,使用這種寫法當然是沒問題的,但問題是Engine是一個介面,Hilt肯定是無法知道要如何創建這個介面的實體,因此這樣寫一定會報錯,

下面我們就來看看該如何一步步解決這個問題,

首先,剛才撰寫的GasEngine和ElectricEngine這兩個實作類,它們是可以依賴注入的,因為它們都有建構式,

因此分別修改GasEngine和ElectricEngine中的代碼,如下所示:

class GasEngine @Inject constructor() : Engine {
    ...
}

class ElectricEngine @Inject constructor() : Engine {
    ...
}

這又是我們剛才學過的技術了,在這兩個類的建構式上分別宣告@Inject注解,

接下來我們需要新建一個抽象類,類名叫什么都可以,但是最好要和業務邏輯有相關性,因此我建議起名EngineModule.kt,如下所示:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

}

這里注意,我們需要在EngineModule的上方宣告一個@Module注解,表示這一個用于提供依賴注入實體的模塊,

如果你之前學習過Dagger2,那么對于這部分理解起來一定會相當輕松,這完全就是和Dagger2是一模一樣的嘛,

而如果你之前沒有學習過Dagger2,也沒有關系,跟著接下來的步驟一步步實作,你自然就能明白它的作用了,

另外可能你會注意到,除了@Module注解之外,這里還宣告了一個@InstallIn注解,這個就是Dagger2中沒有的東西了,關于@InstallIn注解的作用,待會我會使用一塊單獨的主題進行講解,暫時你只要知道必須這么寫就可以了,

定義好了EngineModule之后,接下來我們需要在這個模塊當中提供Engine介面所需要的實體,怎么提供呢?非常簡單,代碼如下所示:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

    @Binds
    abstract fun bindEngine(gasEngine: GasEngine): Engine

}

這里有幾個關鍵的點我逐個說明一下,

首先我們要定義一個抽象函式,為什么是抽象函式呢?因為我們并不需實作具體的函式體,

其次,這個抽象函式的函式名叫什么都無所謂,你也不會呼叫它,不過起個好點的名字可以有助于你的閱讀和理解,

第三,抽象函式的回傳值必須是Engine,表示用于給Engine型別的介面提供實體,那么提供什么實體給它呢?抽象函式接收了什么引數,就提供什么實體給它,由于我們的卡車還比較傳統,使用的仍然是燃油引擎,所以bindEngine()函式接收了GasEngine引數,也就是說,會將GasEngine的實體提供給Engine介面,

最后,在抽象函式上方加上@Bind注解,這樣Hilt才能識別它,

經過一系列的代碼撰寫之后,我們再回到Truck類當中,你會發現,這個時候我們再向engine欄位去進行依賴注入就變得有道理了,因為借助剛才定義的EngineModule,很明顯將會注入一個GasEngine的實體到engine欄位當中,

實際是不是這樣呢?我們來操作一下就知道了,修改Truck類中的代碼,如下所示:

class Truck @Inject constructor(val driver: Driver) {
    
    @Inject
    lateinit var engine: Engine

    fun deliver() {
        engine.start()
        println("Truck is delivering cargo. Driven by $driver")
        engine.shutdown()
    }

}

我們在開始送貨之前先啟動車輛引擎,然后在送貨完成之后完畢車輛引擎,非常合理的邏輯,

現在重新運行一下程式,控制臺列印資訊如圖所示:

正如我們所預期的那樣,在送貨的前后分別列印了燃油引擎啟動和燃油引擎關閉的日志,說明Hilt確實向engine欄位注入了一個GasEngine的實體,

這樣也就解決了給介面進行依賴注入的問題,


給相同型別注入不同的實體

友情提醒,別忘了剛才我們定義的ElectricEngine還沒用上呢,

現在卡車配送公司通過送貨賺到了很多錢,解決了溫飽問題,就該考慮環保問題了,用燃油引擎來送貨實在是不夠環保,為了拯救地球,我們決定對卡車進行升級改造,

但是目前電動車還不夠成熟,存在續航里程短,充電時間長等問題,怎么辦呢?于是我們準備采取一個折中的方案,暫時使用混動引擎來進行過渡,

也就是說,一輛卡車中將會同時包含燃油引擎和電動引擎,

那么問題來了,我們通過EngineModule中的bindEngine()函式為Engine介面提供實體,這個實體要么是GasEngine,要么是ElectricEngine,怎么能同時為一個介面提供兩種不同的實體呢?

可能你會想到,那我定義兩個不同的函式,分別接收GasEngine和ElectricEngine引數不就行了,代碼如下所示:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

    @Binds
    abstract fun bindGasEngine(gasEngine: GasEngine): Engine
    
    @Binds
    abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

}

這種寫法看上去好像挺有道理,但是如果你編譯一下就會發現報錯了:

注意紅框中的文字即可,這個錯誤在提醒我們,Engine被系結了多次,

其實想想也有道理,我們在EngineModule中提供了兩個不同的函式,它們的回傳值都是Engine,那么當在Truck中給engine欄位進行依賴注入時,到底是使用bindGasEngine()函式提供的實體呢?還是使用bindElectricEngine()函式提供的實體呢?Hilt也搞不清楚了,

因此這個問題需要借助額外的技術手段才能解決:Qualifier注解,

Qualifier注解的作用就是專門用于解決我們目前碰到的問題,給相同型別的類或介面注入不同的實體,

這里我們分別定義兩個注解,如下所示:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine

一個注解叫BindGasEngine,一個注解叫BindElectricEngine,這樣兩個注解的作用就明顯區分開了,

另外,注解的上方必須使用@Qualifier進行宣告,這個是毫無疑問的,至于另外一個@Retention,是用于宣告注解的作用范圍,選擇AnnotationRetention.BINARY表示該注解在編譯之后會得到保留,但是無法通過反射去訪問這個注解,這應該是最合理的一個注解作用范圍,

定義好了上述兩個注解之后,我們再回到EngineModule當中,現在就可以將剛才定義的兩個注解分別添加到bindGasEngine()和bindElectricEngine()函式的上方,如下所示:

@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {

    @BindGasEngine
    @Binds
    abstract fun bindGasEngine(gasEngine: GasEngine): Engine

    @BindElectricEngine
    @Binds
    abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine

}

如此一來,我們就將兩個為Engine介面提供實體的函式進行了分類,一個分到了@BindGasEngine注解上,一個分到了@BindElectricEngine注解上,

不過現在還沒結束,因為增加了Qualifier注解之后,所有為Engine型別進行依賴注入的地方也需要去宣告注解,明確指定自己希望注入哪種型別的實體,

因此我們還需要修改Truck類中的代碼,如下所示:

class Truck @Inject constructor(val driver: Driver) {

    @BindGasEngine
    @Inject
    lateinit var gasEngine: Engine

    @BindElectricEngine
    @Inject
    lateinit var electricEngine: Engine

    fun deliver() {
        gasEngine.start()
        electricEngine.start()
        println("Truck is delivering cargo. Driven by $driver")
        gasEngine.shutdown()
        electricEngine.shutdown()
    }

}

這段代碼現在看起來是不是很容易理解了呢?

我們定義了gasEngine和electricEngine這兩個欄位,它們的型別都是Engine,但是在gasEngine的上方,使用了@BindGasEngine注解,這樣Hilt就會給它注入GasEngine的實體,在electricEngine的上方,使用了@BindElectricEngine注解,這樣Hilt就會給它注入ElectricEngine的實體,

最后在deliver()當中,我們先啟動燃油引擎,再啟動電動引擎,送貨結束后,先關閉燃油引擎,再關閉電動引擎,

最終的結果會是什么樣呢?運行一下看看吧,如下圖所示,

非常棒,一切正如我們所預期地那樣運行了,

這樣也就解決了給相同型別注入不同實體的問題,


第三方類的依賴注入

卡車這個例子暫時先告一段落,接下來我們看一些更加實際的例子,

剛才有說過,如果我們想要在MainActivity中使用OkHttp發起網路請求,通常會創建一個OkHttpClient的實體,不過原則上OkHttpClient的實體又不應該由Activity去創建,那么很明顯,這個時候使用依賴注入是一個非常不錯的解決方案,即,讓MainActivity去依賴OkHttpClient即可,

但是這又會引出一個新的問題,OkHttpClient這個類是由OkHttp庫提供的啊,我們并沒有這個類的撰寫權限,因此自然也不可能在OkHttpClient的建構式中加上@Inject注解,那么要如何對它進行依賴注入呢?

這個時候又要借助@Module注解了,它的解決方案有點類似于剛才給介面型別提供依賴注入,但是并不完全一樣,

首先定義一個叫NetworkModule的類,代碼如下所示:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
    
}

它的初始宣告和剛才的EngineModule非常相似,只不過這里沒有將它宣告成抽象類,因為我們不會在這里定義抽象函式,

很明顯,在NetworkModule當中,我們希望給OkHttpClient型別提供實體,因此可以撰寫如下代碼:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {

    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .build()
    }

}

同樣,provideOkHttpClient()這個函式名是隨便定義的,Hilt不做任何要求,但是回傳值必須是OkHttpClient,因為我們就是要給OkHttpClient型別提供實體嘛,

注意,不同的地方在于,這次我們寫的不是抽象函式了,而是一個常規的函式,在這個函式中,按正常的寫法去創建OkHttpClient的實體,并進行回傳即可,

最后,記得要在provideOkHttpClient()函式的上方加上@Provides注解,這樣Hilt才能識別它,

好了,現在如果你想要在MainActivity中去依賴注入OkHttpClient,只需要這樣寫即可:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var okHttpClient: OkHttpClient
    ...

}

然后你可以在MainActivity的任何地方去使用okHttpClient物件,代碼一定會正常運行的,

這樣我們就解決了給第三方庫的類進行依賴注入的問題,不過這個問題其實還可以再進一步拓展一下,

現在直接使用OkHttp的人已經越來越少了,更多的開發者選擇使用Retrofit來作為他們的網路請求解決方案,而Retrofit實際上也是基于OkHttp的,

為了方便開發者的使用,我們希望在NetworkModule中給Retrofit型別提供實體,而在創建Retrofit實體的時候,我們又可以選擇讓其依賴OkHttpClient,具體要怎么寫呢?特別簡單:

@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {

    ...

    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl("http://example.com/")
            .client(okHttpClient)
            .build()
    }

}

這里定義了一個provideRetrofit()函式,然后在函式中按常規的方式去創建Retrofit的實體,并將其回傳即可,

但是我們注意到,provideRetrofit()函式還接收了一個OkHttpClient引數,并且我們在創建Retrofit實體的時候還依賴了這個引數,那么你可能會問了,我們要如何向provideRetrofit()函式去傳遞OkHttpClient這個引數呢?

答案是,完全不需要傳遞,因為這個程序是由Hilt自動完成的,我們所需要做的,就是保證Hilt能知道如何得到一個OkHttpClient的實體,而這個作業我們早在前面一步就已經完成了,

所以,假如現在你在MainActivity中去撰寫這樣的代碼:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var retrofit: Retrofit
    ...

}

絕對是沒有問題的,


Hilt內置組件和組件作用域

剛才我們在學習給介面和第三方類進行依賴注入時,跳過了@InstallIn這個注解,現在是時候該回頭看一下了,

其實這個注解的名字起得還是相當準確的,InstallIn,就是安裝到的意思,那么@InstallIn(ActivityComponent::class),就是把這個模塊安裝到Activity組件當中,

既然是安裝到了Activity組件當中,那么自然在Activity中是可以使用由這個模塊提供的所有依賴注入實體,另外,Activity中包含的Fragment和View也可以使用,但是除了Activity、Fragment、View之外的其他地方就無法使用了,

比如說,我們在Service中使用@Inject來對Retrofit型別的欄位進行依賴注入,就一定會報錯,

不過不用慌,這些都是有辦法解決的,

Hilt一共內置了7種組件型別,分別用于注入到不同的場景,如下表所示,

這張表中,每個組件的作用范圍都不相同,其中,ApplicationComponent提供的依賴注入實體可以在全專案中使用,因此,如果我們希望剛才在NetworkModule中提供的Retrofit實體也能在Service中進行依賴注入,只需要這樣修改就可以了:

@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {
    ...
}

另外和Hilt內置組件相關的,還有一個叫組件作用域的概念,我們也要學習一下它的作用,

或許Hilt的這個行為和你預想的并不一致,但是這確實就是事實:Hilt會為每次的依賴注入行為都創建不同的實體,

這種默認行為在很多時候確實是非常不合理的,比如我們提供的Retrofit和OkHttpClient的實體,理論上它們全域只需要一份就可以了,每次都創建不同的實體明顯是一種不必要的浪費,

而更改這種默認行為其實也很簡單,借助@Singleton注解即可,如下所示:

@Module
@InstallIn(ApplicationComponent::class)
class NetworkModule {

    @Singleton
    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(20, TimeUnit.SECONDS)
            .readTimeout(20, TimeUnit.SECONDS)
            .writeTimeout(20, TimeUnit.SECONDS)
            .build()
    }

    @Singleton
    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .baseUrl("http://example.com")
            .client(okHttpClient)
            .build()
    }
    
}

這樣就可以保證OkHttpClient和Retrofit在全域都只會存在一份實體了,

Hilt一共提供了7種組件作用域注解,和剛才的7個內置組件分別是一一對應的,如下表所示,

也就是說,如果想要在全程式范圍內共用某個物件的實體,那么就使用@Singleton,如果想要在某個Activity,以及它內部包含的Fragment和View中共用某個物件的實體,那么就使用@ActivityScoped,以此類推,

另外,我們不必非得在某個Module中使用作用域注解,也可以直接將它宣告到任何可注入類的上方,比如我們對Driver類進行如下宣告:

@Singleton
class Driver @Inject constructor() {
}

這就表示,Driver在整個專案的全域范圍內都會共享同一個實體,并且全域都可以對Driver類進行依賴注入,

而如果我們將注解改成@ActivityScoped,那么就表示Driver在同一個Activity內部將會共享同一個實體,并且Activity、Fragment、View都可以對Driver類進行依賴注入,

你可能會好奇,這個包含關系是如何確定的,為什么宣告成@ActivityScoped的類在Fragment和View中也可以進行依賴注入?

關于包含關系的定義,我們來看下面這張圖就一目了然了:

簡單來講,就是對某個類宣告了某種作用域注解之后,這個注解的箭頭所能指到的地方,都可以對該類進行依賴注入,同時在該范圍內共享同一個實體,

比如@Singleton注解的箭頭可以指向所有地方,而@ServiceScoped注解的箭頭無處可指,所以只能限定在Service自身當中使用,@ActivityScoped注解的箭頭可以指向Fragment、View當中,

這樣你應該就將Hilt的內置組件以及組件作用域的相關知識都掌握牢了,


預置Qualifier

Android開發相比于傳統的Java開發有其特有的特殊性,比如說Android中有個Context的概念,

剛入門Android開發的新手可能總會疑惑Context到底是什么,而做過多年Android開發的人估計根本就不關心這個問題了,我天天都在用,甚至到處都在用它,對Context是什么已經麻木了,

確實,Android開發中有太多的地方要依賴于Context,動不動呼叫的什么介面就會要求你傳入Context引數,

那么,如果有個我們想要依賴注入的類,它又是依賴于Context的,這個情況要如何解決呢?

舉個例子,現在Driver類的建構式接收一個Context引數,如下所示:

@Singleton
class Driver @Inject constructor(val context: Context) {
}

現在你編譯一下專案一定會報錯,原因也很簡單,Driver類無法被依賴注入了,因為Hilt不知道要如何提供Context這個引數,

感覺似曾相識是不是?好像我們讓Truck類去依賴Driver類的時候也遇到了這個問題,當時的解決方案是在Driver的建構式上宣告@Inject注解,讓其也可以被依賴注入就可以了,

但是很明顯,這里我們不能用同樣的方法解決問題,因為我們根本就沒有Context類的撰寫權限,所以肯定無法在其建構式上宣告@Inject注解,

那么你可能又會想到了,沒有Context類的撰寫權限,那么我們再使用剛才學到的@Module的方式,以第三方類的形式給Context提供依賴注入不就行了?

這種方案乍看之下好像確實可以,但是當你實際去撰寫的時候又會發現問題了,比如說:

@Module
@InstallIn(ApplicationComponent::class)
class ContextModule {
    
    @Provides
    fun provideContext(): Context {
        ???
    }
    
}

這里我定義好了一個ContextModule,定義好了一個provideContext()函式,它的回傳值也確實是Context,但是我接下來不知道該怎么寫了,因為我不能new一個Context的實體去回傳啊,

沒錯,像Context這樣的系統組件,它的實體都是由Android系統去創建的,我們不可以隨便去new它的實體,所以自然也就不能用前面所學的方案去解決,

那么要如何解決呢?非常簡單,Android提供了一些預置Qualifier,專門就是用于給我們提供Context型別的依賴注入實體的,

比如剛才的Truck類,其實只需要在Context引數前加上一個@ApplicationContext注解,代碼就能編譯通過了,如下所示:

@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}

這種寫法Hilt會自動提供一個Application型別的Context給到Truck類當中,然后Truck類就可以使用這個Context去撰寫具體的業務邏輯了,

但是如果你說,我需要的并不是Application型別的Context,而是Activity型別的Context,也沒有問題,Hilt還預置了另外一種Qualifier,我們使用@ActivityContext即可:

@Singleton
class Driver @Inject constructor(@ActivityContext val context: Context) {
}

不過這個時候如果你編譯一下專案,會發現報錯了,原因也很好理解,現在我們的Driver是Singleton的,也就是全域都可以使用,但是卻依賴了一個Activity型別的Context,這很明顯是不可能的,

至于解決方案嘛,相信學了上一塊主題的你一定已經知道了,我們將Driver上方的注解改成@ActivityScoped、@FragmentScoped、@ViewScoped,或者直接刪掉都可以,這樣再次編譯就不會報錯了,

關于預置Qualifier其實還有一個隱藏的小技巧,就是對于Application和Activity這兩個型別,Hilt也是給它們預置好了注入功能,也就是說,如果你的某個類依賴于Application或者Activity,不需要想辦法為這兩個類提供依賴注入的實體,Hilt自動就能識別它們,如下所示:

class Driver @Inject constructor(val application: Application) {
}

class Driver @Inject constructor(val activity: Activity) {
}

這種寫法編譯將可以直接通過,無需添加任何注解宣告,

注意必須是Application和Activity這兩個型別,即使是宣告它們的子型別,編譯都無法通過,

那么你可能會說,我的專案會在自定義的MyApplication中提供一些全域通用的函式,導致很多地方都是要依賴于我自己撰寫的MyApplication的,而MyApplication又不能被Hilt識別,這種情況要怎么辦呢?

這里我教大家一個小竅門,因為Application全域只會存在一份實體,因此Hilt注入的Application實體其實就是你自定義的MyApplication實體,所以想辦法做一下向下型別轉換就可以了,

比如說這里我定義了一個ApplicationModule,代碼如下所示:

@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {

    @Provides
    fun provideMyApplication(application: Application): MyApplication {
        return application as MyApplication
    }

}

可以看到,provideMyApplication()函式中接收一個Application引數,這個引數Hilt是自動識別的,然后我們將其向下轉型成MyApplication即可,

接下來你在Truck類中就可以去這樣宣告依賴了:

class Driver @Inject constructor(val application: MyApplication) {
}

完美解決,


ViewModel的依賴注入

到目前為止,你已經將Hilt中幾乎所有的重要知識點都學習完了,

做事情講究有始有終,讓我們回到開始時候的一個話題:在MVVM架構中,倉庫層的實體到底應該由誰來創建?

這個問題現在你有更好的答案了嗎?

我在學完Hilt之后,這個問題就已經釋懷了,很明顯,根據MVVM的架構示意圖,ViewModel層只是依賴于倉庫層,它并不關心倉庫的實體是從哪兒來的,因此由Hilt去管理倉庫層的實體創建再合適不過了,

至于具體該如何實作,我總結下來大概有兩種方式,這里分別跟大家演示一下,

注意,以下代碼只是做了MVVM架構中與依賴注入相關部分的演示,如果你還沒有了解過MVVM架構,或者沒有了解過Jetpack組件,可能會看不懂下面的代碼,這部分朋友建議先去參考 《第一行代碼 Android 第3版》的第13和第15章,

第一種方式就是純粹利用我們前面所學過的知識自己手寫,

比如說我們有一個Repository類用于表示倉庫層:

class Repository @Inject constructor() {
    ...
}

由于Repository要依賴注入到ViewModel當中,所以我們需要給Repository的建構式加上@Inject注解,

然后有一個MyViewModel繼承自ViewModel,用于表示ViewModel層:

@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
    ...
}

這里注意以下三點,

第一,MyViewModel的頭部要為其宣告@ActivityRetainedScoped注解,參照剛才組件作用域那張表,我們知道這個注解就是專門為ViewModel提供的,并且它的生命周期也和ViewModel一致,

第二,MyViewModel的建構式中要宣告@Inject注解,因為我們在Activity中也要使用依賴注入的方式獲得MyViewModel的實體,

第三,MyViewModel的建構式中要加上Repository引數,表示MyViewModel是依賴于Repository的,

接下來就很簡單了,我們在MainActivity中通過依賴注入的方式得到MyViewModel的實體,然后像往常一樣的方式去使用它就可以了:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var viewModel: MyViewModel
    ...
    
}

這種方式雖然可以正常作業,但有個缺點是,我們改變了獲取ViewModel實體的常規方式,本來我只是想對Repository進行依賴注入的,現在連MyViewModel也要跟著一起依賴注入了,

為此,對于ViewModel這種常用Jetpack組件,Hilt專門為其提供了一種獨立的依賴注入方式,也就是我們接下來要介紹的第二種方式了,

這種方式我們需要在app/build.gradle檔案中添加兩個額外的依賴:

dependencies {
    ...
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
}

然后修改MyViewModel中的代碼,如下所示:

class MyViewModel @ViewModelInject constructor(val repository: Repository) : ViewModel() {
    ...
}

注意這里的變化,首先@ActivityRetainedScoped這個注解不見了,因為我們不再需要它了,其次,@Inject注解變成了@ViewModelInject注解,從名字上就可以看出,這個注解是專門給ViewModel使用的,

現在回到MainActivity當中,你就不再需要使用依賴注入的方式去獲取MyViewModel的實體了,而是完全按照常規的寫法去獲取即可:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
    ...

}

看上去和我們平時使用ViewModel時的寫法完全無二,這都是由Hilt在背后幫我們施了神奇的魔法,

需要注意的是,這種寫法下,雖然我們在MainActivity里沒有使用依賴注入功能,但是@AndroidEntryPoint這個注解仍然是不能少的,不然的話,在編譯時期Hilt確實檢測不出來語法上的例外,一旦到了運行時期,Hilt找不到入口點就無法執行依賴注入了,


不支持的入口點怎么辦?

在最開始學習Hilt的時候,我就提到了,Hilt一共支持6個入口點,分別是:

  • Application
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

之所以做這樣的設定,是因為我們的程式基本都是由這些入口點出發的,

比如一個Android程式肯定不可能憑空從Truck類開始執行代碼,而一定要從上述的某個入口點開始執行,然后才能輾轉執行到Truck類中的代碼,

但是不知道你有沒有發現,Hilt支持的入口點中少了一個關鍵的Android組件:ContentProvider,

我們都知道,ContentProvider是四大組件之一,并且它也是可以稱之為一個入口點的,因為代碼可以從這里開始直接運行,而并不需要經過其他類的呼叫才能到達它,

那么為什么Hilt支持的入口點中不包括ContentProvider呢?這個問題我也很疑惑,所以在上次的上海GDG圓桌會議上,我將這個問題直接提給了Yigit Boyar,畢竟他在Google是專門負責Jetpack專案的,

當然我也算得到了一個比較滿意的回答,主要原因就是ContentProvider的生命周期問題,如果你比較了解ContentProvider的話,應該知道它的生命周期是比較特殊的,它在Application的onCreate()方法之前就能得到執行,因此很多人會利用這個特性去進行提前初始化,詳見 Jetpack新成員,App Startup一篇就懂 這篇文章,

而Hilt的作業原理是從Application的onCreate()方法中開始的,也就是說在這個方法執行之前,Hilt的所有功能都還無法正常作業,

也正是因為這個原因,Hilt才沒有將ContentProvider納入到支持的入口點當中,

不過,即使ContentProvider并不是入口點,我們仍然還有其他辦法在其內部使用依賴注入功能,只是要稍微麻煩一點,

首先可以在ContentProvider中自定義一個自己的入口點,并在其中定義好要依賴注入的型別,如下所示:

class MyContentProvider : ContentProvider() {

    @EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface MyEntryPoint {
        fun getRetrofit(): Retrofit
    }
    ...
  
}

可以看到,這里我們定義了一個MyEntryPoint介面,然后在其上方使用@EntryPoint來宣告這是一個自定義入口點,并用@InstallIn來宣告其作用范圍,

接著我們在MyEntryPoint中定義了一個getRetrofit()函式,并且函式的回傳型別就是Retrofit,

而Retrofit是我們已支持依賴注入的型別,這個功能早在NetworkModule當中就已經完成了,

現在,如果我們想要在MyContentProvider的某個函式中獲取Retrofit的實體(事實上,ContentProvider中不太可能會用到網路功能,這里只是舉例),只需要這樣寫就可以了:

class MyContentProvider : ContentProvider() {

    ...
    override fun query(...): Cursor {
        context?.let {
            val appContext = it.applicationContext
            val entryPoint = EntryPointAccessors.fromApplication(appContext, MyEntryPoint::class.java)
            val retrofit = entryPoint.getRetrofit()
        }
        ...
    }

}

借助EntryPointAccessors類,我們呼叫其fromApplication()函式來獲得自定義入口點的實體,然后再呼叫入口點中定義的getRetrofit()函式就能得到Retrofit的實體了,

不過我認為,自定義入口點這個功能在實際開發當中并不常用,這里只是考慮知識完整性的原因,所以將這塊內容也加入了進來,


結尾

到這里,這篇文章總算是結束了,

不愧稱它是一篇我自己都怕的文章,這篇文章大概花了我半個月左右的時間,可能是我寫過的最長的一篇文章,

由于Hilt涉及的知識點繁多,即使它將Dagger2的用法進行了大幅的簡化,但如果你之前對于依賴注入完全沒有了解,直接上手Hilt相信還是會有不少的困難,

我在本文當中盡可能地將 “什么是依賴注入,為什么要使用依賴注入,如何使用依賴注入” 這幾個問題描述清楚了,但介于依賴注入這個話題本身復雜度的客觀原因,我也不知道本文的難易程度到底在什么等級,希望閱讀過的讀者朋友們都能達到掌握Hilt,并用好Hilt的水平吧,

另外,由于Hilt和Dagger2的關系過于緊密,我們在本文中所學的知識,有些是Hilt提供的,有些是Dagger2本身就自帶,但是我對此在文中并沒有進行嚴格的區分,統一都是以Hilt的視角去講的,所以,熟悉Dagger2的朋友請不要覺得文中的說法不夠嚴謹,因為太過嚴謹的話可能會增加沒有學過Dagger2這部分讀者朋友的理解成本,

最后,我將本文中用到的一些代碼示例,寫成了一個Demo程式上傳到了GitHub上,有需要的朋友直接去下載原始碼即可,

https://github.com/guolindev/HiltSample


關注我的技術公眾號,每天都有優質技術文章推送,

微信掃一掃下方二維碼即可關注:

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/227514.html

標籤:其他

上一篇:作業五年,面試官說我只會CRUD!竟然只給我10K!

下一篇:vue實作世界疫情地圖(點擊進入子地圖)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more