Jetpack - Hilt
- 依賴注入、依賴注入框架
- Android 常用的依賴注入框架
- Hilt 的簡(jiǎn)單使用

1. 依賴注入、依賴注入框架
1.1 依賴注入
依賴注入的英文名是 Dependency Injection,簡(jiǎn)稱 DI。其作用一言以蔽之:解耦。
舉個(gè)栗子:
有一家卡車配送公司,只有一輛卡車用來(lái)送貨。接到一個(gè)配送訂單,客戶委托配送兩臺(tái)電腦??删帉懭缦麓a:
// 定義一個(gè)卡車 Truck,卡車有一個(gè) deliver() 函數(shù)用于執(zhí)行配送任務(wù)
// 在 deliver() 函數(shù)中先把兩臺(tái)電腦裝上卡車,再進(jìn)行配送
class Truck {
private val computer1 = Computer()
private val computer2 = Computer()
fun driver() {
loadToTruck(computer1)
loadToTruck(computer2)
beginToDeliver()
}
}
上面在 Truck 類中創(chuàng)建了兩臺(tái)電腦的實(shí)例,然后再對(duì)它們進(jìn)行配送??ㄜ嚰纫獣?huì)送貨,也要會(huì)生產(chǎn)電腦,使得卡車和電腦這兩樣不相干的東西耦合到一起去了,造成耦合度過(guò)高。
若又接到一個(gè)新的訂單,去配送手機(jī),那這輛卡車還要會(huì)生產(chǎn)手機(jī)才行。若增加配送蔬果的訂單,那么這輛卡車還要會(huì)種地。。。最后發(fā)現(xiàn),這已經(jīng)就不是一輛卡車了,而是一個(gè)商品制造中心了:

其實(shí),卡車并不需要關(guān)心配送的貨物具體是什么,它的任務(wù)只需負(fù)責(zé)送貨。即卡車是依賴于貨物的,給了卡車貨物,它就去送貨,不給卡車貨物,它就待命,修改代碼如下:
// 在Truck類中添加了貨物cargos字段,卡車是依賴于貨物的
class Truck {
lateinit var cargos: List<Cargo>
fun driver() {
for(cargo in cargos) {
loadToTruck(cargo)
}
beginToDeliver()
}
}
這樣,卡車不再關(guān)心任何商品制造的事情,而是依賴了什么貨物,就去配送什么貨物,只做本職應(yīng)該做的事情。
這種讓外部幫卡車初始化需要配送的貨物的寫法,就稱之為:依賴注入。即讓外部幫你初始化你的依賴,就叫依賴注入。
1.2 依賴注入框架
目前 Truck 類設(shè)計(jì)得比較合理了,但還存在問(wèn)題。
若此時(shí)身份變成了一家電腦公司老板,該如何讓一輛卡車來(lái)幫忙運(yùn)送電腦呢?也許會(huì)很自然的寫出如下代碼:
class ComputerCompany {
private val computer1 = Computer()
private val computer2 = Computer()
fun deliverByTruck() {
val truck = Truck()
truck.cargos = listof(computer1, computer2)
truck.deliver()
}
}
上面代碼同樣也存在高耦合度問(wèn)題:在 deliverByTruck() 函數(shù)中,為了讓卡車送貨,自己制造了一輛卡車。這明顯是不合理的,電腦公司應(yīng)該只負(fù)責(zé)生產(chǎn)電腦,它不應(yīng)該去生產(chǎn)卡車。
更加合理的做法是,讓卡車配送公司派輛空閑的卡車過(guò)來(lái)(就不用自己造車了),當(dāng)卡車到達(dá)后,再將電腦裝上卡車,然后執(zhí)行配送任務(wù)即可。如下:

使用這種設(shè)計(jì)結(jié)構(gòu),就有很好的擴(kuò)展性。若現(xiàn)在又有一家蔬果公司需要找一輛卡車來(lái)送菜,就完全可以使用同樣的結(jié)構(gòu)來(lái)完成任務(wù),如下:

上圖中呼叫卡車公司并讓他們安排空閑車輛的這個(gè)部分,其實(shí)可以通過(guò)自己手寫來(lái)實(shí)現(xiàn),也可借助一些依賴注入框架來(lái)簡(jiǎn)化這個(gè)過(guò)程。
因此,依賴注入框架的作用就是為了替換下圖所示的部分:

2. Android常用的依賴注入框架
2.1 Dagger
由Square公司開(kāi)源,基于Java反射去實(shí)現(xiàn)的,從而有兩個(gè)潛在的隱患:
反射是比較耗時(shí)的,用這種方式會(huì)降低程序的運(yùn)行效率。(這問(wèn)題不大,現(xiàn)在的程序中到處都在用反射)
依賴注入框架的用法總體來(lái)說(shuō)比較有難度,很難一次性編寫正確。而基于反射實(shí)現(xiàn)的依賴注入功能,在編譯期無(wú)法得知依賴注入的用法是否正確,只能在運(yùn)行時(shí)通過(guò)程序是否崩潰來(lái)判斷。這樣測(cè)試的效率低下,容易將一些 bug 隱藏得很深。
2.2 Dagger2
由 Google 開(kāi)發(fā),基于 Java 注解實(shí)現(xiàn)的,把 Dagger1 反射的那些弊端解決了:
通過(guò)注解,Dagger2 會(huì)在編譯時(shí)期自動(dòng)生成用于依賴注入的代碼,不會(huì)增加任何運(yùn)行耗時(shí)。另外,Dagger2 會(huì)在編譯時(shí)檢查依賴注入用法是否正確,若不正確則會(huì)直接編譯失敗,從而將問(wèn)題盡可能早地拋出。即項(xiàng)目正常編譯通過(guò),說(shuō)明依賴注入的用法基本沒(méi)問(wèn)題了。
但 Dagger2 使用比較復(fù)雜,若不能很好地使用它,可能會(huì)拖累你的項(xiàng)目,甚至?xí)⒁恍┖?jiǎn)單的項(xiàng)目過(guò)度設(shè)計(jì)。
2.3 Hilt
Google 發(fā)布了 Hilt,是在依賴項(xiàng)注入庫(kù) Dagger 的基礎(chǔ)上構(gòu)建而成,一個(gè)專門面向 Android 的依賴注入框架。
相比于 Dagger2,Hilt 最明顯的特征就是: 簡(jiǎn)單、提供了 Android 專屬的 API。
3. Hilt 的簡(jiǎn)單使用
3.1 引入Hilt
第一步,在項(xiàng)目根目錄的 build.gradle 文件中配置 Hilt 的插件路徑:
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.31.1-alpha'
}
}
接下來(lái),在 app/build.gradle 文件中,引入 Hilt 的插件并添加 Hilt 的依賴庫(kù):
plugins {
...
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
// hilt
implementation "com.google.dagger:hilt-android:2.31.1-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.31.1-alpha"
}
最后,Hilt 使用 Java 8 功能。如需啟用 Java 8,在 app/build.gradle 文件中添加以下代碼:
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
這就成功將 Hilt 引入到項(xiàng)目當(dāng)中了。
3.2 Hilt 的簡(jiǎn)單用法
在 Hilt 當(dāng)中,必須要自定義一個(gè) Application 才行,否則 Hilt 將無(wú)法正常工作。如下:
// 注解 @HiltAndroidApp 會(huì)觸發(fā) Hilt 的代碼生成操作,
// 生成的代碼包括應(yīng)用的一個(gè)基類,該基類充當(dāng)應(yīng)用級(jí)依賴項(xiàng)容器。
@HiltAndroidApp
class MyApplication: Application() {
}
在 Application 類中設(shè)置了 Hilt 且有了應(yīng)用級(jí)組件后,Hilt 可以為帶有 @AndroidEntryPoint 注解的其他 Android 類提供依賴項(xiàng)。
Hilt 目前支持以下 Android 類:
Application(通過(guò)使用 @HiltAndroidApp)
Activity、Fragment、View、Service、BroadcastReceiver(通過(guò)使用 @AndroidEntryPoint)
以 Activity 為例,在 MainActivity 中進(jìn)行依賴注入:
@AndroidEntryPoint
class MainActivity: AppCompatActivity() {
}
如把上面的 Truck 類注入到 MainActivity 當(dāng)中:
@AndroidEntryPoint
class MainActivity: AppCompatActivity() {
// 步驟二:在 truck 字段的上方聲明了一個(gè) @Inject 注解
// 即希望通過(guò) Hilt 來(lái)注入 truck 這個(gè)字段
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
truck.driver()
}
}
// 步驟一:在 Truck 類的構(gòu)造函數(shù)上聲明了一個(gè) @Inject 注解
// 即告訴 Hilt,可以通過(guò)這個(gè)構(gòu)造函數(shù)來(lái)安排一輛卡車
class Truck @Inject constructor() {
fun driver() {
println("卡車運(yùn)輸貨物")
}
}
這樣在 MainActivity 中并沒(méi)有去創(chuàng)建 Truck 的實(shí)例,只是用 @Inject 聲明一下,就可以調(diào)用它的 deliver() 方法,即用 Hilt 完成了依賴注入的功能。
注:Hilt 注入的字段是不可以聲明成 private 的。
3.3 帶參數(shù)的依賴注入
比如在 Truck 類的構(gòu)造函數(shù)中增加了一個(gè) Driver 參數(shù):
class Truck @Inject constructor(val driver: Driver) {
fun driver() {
println("卡車運(yùn)輸貨物,司機(jī)是 $driver")
}
}
class Driver @Inject constructor() {
}
在 Driver 類的構(gòu)造函數(shù)上聲明了一個(gè) @Inject 注解,這樣 Driver 類就變成了無(wú)參構(gòu)造函數(shù)的依賴注入方式。即 Truck 的構(gòu)造函數(shù)中所依賴的所有其他對(duì)象都支持依賴注入了,那么 Truck 才可以被依賴注入。
注:前面的 MainActivity 無(wú)需修改,Truck 中的 Driver 會(huì)自動(dòng)生成。
3.4 接口的依賴注入
如定義個(gè) Engine 接口和它的實(shí)現(xiàn)類如下:
interface Engine {
fun start()
fun shutdown()
}
class GasEngine @Inject constructor() : Engine {
override fun start() {
println("燃油車 start")
}
override fun shutdown() {
println("燃油車 shutdown")
}
}
接下來(lái)需要定義一個(gè)抽象類,使用 @Binds 注入接口實(shí)例 :
// 在 EngineModule 的上方聲明一個(gè) @Module 注解,表示這一個(gè)用于提供依賴注入實(shí)例的模塊
// @InstallIn(ActivityComponent::class),表示把這個(gè)模塊安裝到Activity組件當(dāng)中
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
// 1. 定義一個(gè)抽象函數(shù)(因?yàn)椴⒉恍鑼?shí)現(xiàn)具體的函數(shù)體)
// 2. 這個(gè)抽象函數(shù)的函數(shù)名叫什么都無(wú)所謂,也不會(huì)調(diào)用它。
// 3. 抽象函數(shù)的返回值必須是Engine,表示用于給Engine類型的接口提供實(shí)例。
// 4. 在抽象函數(shù)上方加上@Bind注解,這樣Hilt才能識(shí)別它。
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
}
定義好抽象類 EngineModule 后,修改 Truck 類的代碼如下:
class Truck @Inject constructor(val driver: Driver) {
@Inject
lateinit var engine: Engine
fun driver() {
engine.start()
println("卡車運(yùn)輸貨物,司機(jī)是 $driver")
engine.shutdown()
}
}
這樣,Hilt 就向 engine 字段注入了一個(gè) GasEngine 的實(shí)例,也就完成了給接口進(jìn)行依賴注入。
3.5 給相同類型注入不同的實(shí)例
比如再有個(gè) Engine 接口的實(shí)現(xiàn)類:
class ElectricEngine @Inject constructor() : Engine {
override fun start() {
println("新能源車 start")
}
override fun shutdown() {
println("新能源車 shutdown")
}
}
此時(shí),通過(guò) EngineModule 中的 bindEngine() 函數(shù)為 Engine 接口提供實(shí)例,這個(gè)實(shí)例要么是 GasEngine,要么是 ElectricEngine,如何同時(shí)為一個(gè)接口提供兩種不同的實(shí)例呢?
這時(shí)就要借助 Qualifier注解 來(lái)解決。Qualifier 注解的作用是給相同類型的類或接口注入不同的實(shí)例。
分別定義兩個(gè)注解,如下:
// 注解的上方必須使用 @Qualifier 進(jìn)行聲明。
// 注解 @Retention,是用于聲明注解的作用范圍
// 選擇 AnnotationRetention.BINARY 表示該注解在編譯之后會(huì)得到保留,但無(wú)法通過(guò)反射去訪問(wèn)這個(gè)注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine
定義好上面兩個(gè)注解后,把它們分別添加到 EngineModule里對(duì)應(yīng)的方法中:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@BindGasEngine
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
最后修改 Truck 類中的代碼如下:
class Truck @Inject constructor(val driver: Driver) {
@BindGasEngine
@Inject
lateinit var gasEngine: Engine
@BindElectricEngine
@Inject
lateinit var electricEngine: Engine
fun driver() {
gasEngine.start()
electricEngine.start()
println("卡車運(yùn)輸貨物,司機(jī)是 $driver")
gasEngine.shutdown()
electricEngine.shutdown()
}
}
這樣就完成了給相同類型注入不同實(shí)例。
3.6 第三方類的依賴注入
給第三方類的依賴注入需要使用 @Provides 注解,如給 OkHttpClient、Retrofit 類型提供實(shí)例如下:
@Module
@InstallIn(ActivityComponent::class)
class NetModule {
@Provides
fun provideOkHttpClient() : OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.writeTimeout(0, TimeUnit.SECONDS)
.build()
}
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient) : Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http:xxx.com")
.client(okHttpClient)
.build()
}
}
在 provideOkHttpClient()、provideRetrofit() 函數(shù)的上方加上 @Provides 注解,Hilt 就能識(shí)別它。
3.7 Hilt 內(nèi)置組件和組件作用域
Hilt 一共內(nèi)置了7種組件類型,分別用于注入到不同的場(chǎng)景,如 @InstallIn(ActivityComponent::class),就是把這個(gè)模塊安裝到 Activity組件當(dāng)中,如下表:

Hilt 一共提供了7種組件作用域注解,和上面的7個(gè)內(nèi)置組件分別是一一對(duì)應(yīng)的,如下表:

若想要在全程序范圍內(nèi)共用某個(gè)對(duì)象的實(shí)例,那么就使用 @Singleton。
若想要在某個(gè) Activity,以及它內(nèi)部包含的 Fragment 和 View 中共用某個(gè)對(duì)象的實(shí)例,那么就使用@ActivityScoped。
以此類推。。。
作用域的包含關(guān)系如下:

即,對(duì)某個(gè)類聲明了某種作用域注解之后,這個(gè)注解的箭頭所能指到的地方,都可以對(duì)該類進(jìn)行依賴注入,同時(shí)在該范圍內(nèi)共享同一個(gè)實(shí)例。
如 @Singleton 注解的箭頭可以指向所有地方。
如 @ServiceScoped 注解的箭頭無(wú)處可指,所以只能限定在 Service 自身當(dāng)中使用。
如 @ActivityScoped 注解的箭頭可以指向Fragment、View 當(dāng)中。
3.8 Hilt 中的預(yù)定義限定符
Hilt 提供了一些預(yù)定義的限定符。
例如,需要來(lái)自應(yīng)用或 Activity 的 Context 類,就可以用 Hilt 提供的 @ApplicationContext 和 @ActivityContext 限定符。
用法很簡(jiǎn)單,只需要在 Context 參數(shù)前加上一個(gè) @ApplicationContext 注解即可:
@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}
// 這邊 @ApplicationContext 或 @ActivityContext 可以去掉,Hilt 也能識(shí)別
class Driver @Inject constructor(val application: Application) {}
class Driver @Inject constructor(val activity: Activity) {}
若要依賴于自己編寫的 MyApplication 的,可以定義個(gè) ApplicationModule 如下:
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {
@Provides
fun provideMyApplication(application: Application): MyApplication {
return application as MyApplication
}
}
使用如下:
class Driver @Inject constructor(val application: MyApplication) {
}
3.9 ViewModel 的依賴注入
在 MVVM 架構(gòu)中,ViewModel 層只是依賴于倉(cāng)庫(kù)層,它并不關(guān)心倉(cāng)庫(kù)的實(shí)例是從哪兒來(lái)的,因此由 Hilt 去管理倉(cāng)庫(kù)層的實(shí)例創(chuàng)建再合適不過(guò)了。
常見(jiàn)的 ViewModel 依賴注入過(guò)程如下:
首先有個(gè)倉(cāng)庫(kù) Repository 類:
// 由于 Repository 要依賴注入到 ViewModel 當(dāng)中,所以需要加上 @Inject 注解
class Repository @Inject constructor() {
...
}
然后有一個(gè) MyViewModel 繼承自 ViewModel ,用于表示 ViewModel 層:
// @HiltViewModel 注解,是專門為 ViewModel 提供的
// 構(gòu)造函數(shù)中要聲明 @Inject 注解,在 Activity 中才能使用依賴注入的方式獲得 MyViewModel 的實(shí)例
@HiltViewModel
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
...
}
接下來(lái)修改 MainActivity 如下:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MyViewModel
...
}
這樣在 MainActivity 中就可以通過(guò)依賴注入的方式得到 MyViewModel 的實(shí)例了。
當(dāng)然如果有引入類似 Activity 擴(kuò)展庫(kù) ktx:
// ktx
implementation "androidx.activity:activity-ktx:1.1.0"
那么上面 MainActivity 可修改如下:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 此時(shí)無(wú)需聲明 @Inject 注解
private val viewModel: MyViewModel by viewModels()
...
}
以上就是 ViewModel 的依賴注入。
本篇文章就介紹到這。
參考鏈接:
Jetpack新成員,一篇文章帶你玩轉(zhuǎn)Hilt和依賴注入