本文同步發表於我的微信公眾號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每個工作日都有文章更新。
各位小伙伴們大家早上好。
終於要寫這樣一篇我自己都比較怕的文章了。
雖然今年的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
關注我的技術公眾號,每天都有優質技術文章推送。
微信掃一掃下方二維碼即可關注:
![]()