
本文是 MAD Skills 系列 中有關(guān) Hilt 的第一篇文章!在本文中,我們將探討依賴項注入 (DI) 對應用的重要性,以及 Jetpack 推薦的 Android DI 解決方案——Hilt。
如果您更喜歡通過視頻了解此內(nèi)容,可以 點擊這里 查看。
在 Android 應用中,您可以通過遵循依賴項注入的原則,為良好的應用架構(gòu)奠定基礎(chǔ)。這有助于重用代碼、易于重構(gòu)、易于測試!更多關(guān)于 DI 的好處,請參閱: Android 中的依賴項注入。
在項目中創(chuàng)建類的實例時,您可以通過提供及傳遞所需依賴項,手動處理依賴關(guān)系圖。
但是每次都手動執(zhí)行會增加模版代碼并且容易出錯。以 iosched 項目 (Google I/O 開源應用) 中的一個 ViewModel 為例,您能想象創(chuàng)建一個 FeedViewModel 所需的依賴項及傳遞依賴項需要多大的代碼量嗎?
class FeedViewModel(
private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
getTimeZoneUseCase: GetTimeZoneUseCase,
getConferenceStateUseCase: GetConferenceStateUseCase,
private val timeProvider: TimeProvider,
private val analyticsHelper: AnalyticsHelper,
private val signInViewModelDelegate: SignInViewModelDelegate,
themedActivityDelegate: ThemedActivityDelegate,
private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
FeedEventListener,
ThemedActivityDelegate by themedActivityDelegate,
SignInViewModelDelegate by signInViewModelDelegate {
/* ... */
}
這是復雜且機械化的,并且我們很容易弄錯依賴關(guān)系。依賴項注入庫可以讓我們利用 DI 的優(yōu)勢,而無需手動提供依賴關(guān)系,因為庫會幫您生成所有需要的代碼。這也就是 Hilt 發(fā)揮作用的地方。
Hilt
Hilt 是一個由 Google 開發(fā)的依賴項注入庫,它通過處理復雜的依賴關(guān)系并為您生成原本需要手動編寫的模版代碼,幫助您在應用中充分利用 DI 的最佳實踐。
Hilt 通過使用注解在編譯期幫您生成代碼,來保證運行時性能。這是利用 JVM DI 庫 Dagger 的能力實現(xiàn)的,而 Hilt 是基于 Dagger 構(gòu)建的。
Hilt 是 Jetpack 推薦的 Android 應用 DI 解決方案,它附帶工具并且支持其他 Jetpack 庫。
快速開始
所有使用 Hilt 的應用都必須包含被 @HiltAndroidApp 注解的 Application 類,它會在編譯期觸發(fā) Hilt 的代碼生成。為了 Hilt 能將依賴項注入到 Activity 中,Activity 需要使用 @AndroidEntryPoint 注解。
@HiltAndroidApp
class MusicApp : Application()
@AndroidEntryPoint
class PlayActivity : AppCompatActivity() { /* ... */ }
注入一個依賴項時,需要在您希望注入的變量上添加 @Inject 注解。super.onCreate 被調(diào)用后,所有 Hilt 注入的變量都將可用。
@AndroidEntryPoint
class PlayActivity : AppCompatActivity() {
@Inject lateinit var player: MusicPlayer
override fun onCreate(savedInstanceState: Bundle) {
super.onCreate(bundle)
player.play("YHLQMDLG")
}
}
在本案例中,我們向 PlayActivity 內(nèi)注入 MusicPlayer,但是 Hilt 是如何知道怎樣提供 MusicPlayer 類型的實例呢?還需要額外的工作!我們還需要告訴 Hilt 如何處理,當然還是使用注解!
在類的構(gòu)造方法上添加 @Inject 注解,告訴 Hilt 怎樣創(chuàng)建該類的實例。
class MusicPlayer @Inject constructor() {
fun play(id: String) { ... }
}
這就是將依賴項注入到 Activity 中所需的全部內(nèi)容!非常簡單!我們從一個簡單的例子開始,因為 MusicPlayer 并不依賴任何其他類型。但是如果我們將其他依賴作為參數(shù)傳遞,Hilt 會在提供 MusicPlayer 的實例時處理并滿足這些依賴項。
實際上,這是一個非常簡單初級的例子。但是如果您必須手動完成我們上述工作,您會怎樣做?
手動實現(xiàn)
手動執(zhí)行 DI 時,您需要一個依賴項容器,它負責提供類型的實例并管理這些實例的生命周期。簡單的說,這些就是 Hilt 在幕后所做的內(nèi)容。
當我們在 Activity 上添加 @AndroidEntryPoint 注解時,Hilt 會自動創(chuàng)建一個依賴項容器,并管理、關(guān)聯(lián)到 PlayActivity 上。這里我們手動實現(xiàn) PlayActivityContainer 容器。通過在 MusicPlayer上添加 @Inject 注解,等同于告訴容器如何提供 MusicPlayer 的實例。
// PlayActivity 已被添加 @AndroidEntryPoint 注解
class PlayActivityContainer {
// MusicPlayer 已被添加 @Inject 注解
fun provideMusicPlayer() = MusicPlayer()
}
在 Activity 中,我們需要創(chuàng)建一個容器實例,并使用它對 Activity 的依賴項賦值。對于 Hilt 而言,在 Activity 上添加 @AndroidEntryPoint 注解時也完成了容器實例的創(chuàng)建。
class PlayActivity : AppCompatActivity() {
private lateinit var player: MusicPlayer
// 在 Activity 上添加 @AndroidEntryPoint 注解時由 Hilt 創(chuàng)建
private lateinit var container: PlayActivityContainer
override fun onCreate(savedInstanceState: Bundle) {
// @AndroidEntryPoint 同樣為您創(chuàng)建并填充字段
container = PlayActivityContainer()
player = container.provideMusicPlayer()
super.onCreate(bundle)
player.play("YHLQMDLG")
}
}
注解回顧
至此,我們已經(jīng)看見,當 @Inject 注解被添加到類的構(gòu)造函數(shù)上時,它會告訴 Hilt 如何提供該類的實例。當變量被添加 @Inject 注解,并且變量所屬的類被添加 @AndroidEntryPoint 注解時,Hilt 會向該類中注入一個相應類型的實例。
@AndroidEntryPoint 注解可以添加到絕大部分 Android 框架類上,不僅僅是 Activity。它會為被添加注解的類去創(chuàng)建一個依賴項容器的實例,并填充所有添加了 @Inject 注解的變量。
在 Application 類上添加 @HiltAndroidApp 注解,除了觸發(fā) Hilt 生成代碼之外,還創(chuàng)建了一個與 Application 關(guān)聯(lián)的依賴項容器。
Hilt 模塊
我們既然已經(jīng)了解了 Hilt 基礎(chǔ),那一起來提高示例的復雜性吧?,F(xiàn)在,MusicPlayer 的構(gòu)造函數(shù)中,需要一個依賴項 MusicDatabase。
class MusicPlayer @Inject constructor(
private val db: MusicDatabase
) {
fun play(id: String) { ... }
}
因此,我們需要告訴 Hilt 如何提供 MusicDatabase 實例。當類型是一個接口,或者您無法在構(gòu)造函數(shù)上添加 @Inject,例如類來自于您無法修改的庫。
假設(shè)我們在應用中 使用 Room 作為持久性存儲庫?;氐轿覀兪謩訉崿F(xiàn) PlayActivityContainer 的場景中,當我們通過 Room 提供 MusicDatabase 時,這將是一個抽象類,我們希望在提供依賴項時執(zhí)行一些代碼。接下來,當提供 MusicPlayer 的實例時,我們需要調(diào)用提供或者滿足 MusicDatabase 依賴項的方法。
class PlayActivityContainer(val context: Context) {
fun provideMusicDatabase(): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
}
fun provideMusicPlayer() = MusicPlayer(
provideMusicDatabase()
)
}
在 Hilt 中我們無需擔心傳遞依賴,因為它會自動關(guān)聯(lián)所有需要傳遞的依賴項。然而,我們需要讓 Hilt 知道如何提供 MusicDatabase 類型的實例。為此,我們使用 Hilt 模塊。
Hilt 模塊是一個被添加了 @Module 注解的類。在該類中,我們可以實現(xiàn)函數(shù)來告訴 Hilt 如何提供確切類型的實例。Hilt 已知的此類信息在行業(yè)內(nèi)也被稱為綁定。
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Provides
fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
}
}
在該函數(shù)上添加 @Provides 注解用來告訴 Hilt 如何提供 MusicDatabase 類型的實例。函數(shù)體包含 Hilt 需要執(zhí)行的代碼塊,這與我們手動實現(xiàn)完全一致。
返回類型 MusicDatabase 告知 Hilt 此函數(shù)提供什么類型。函數(shù)的參數(shù)告訴 Hilt 該類型所需的依賴項。本案例中,ApplicationContext 已經(jīng)在 Hilt 中可用。這段代碼告知 Hilt 如何提供 MusicDatabase 類型的實例,換句話說,我們已經(jīng)有了一個 MusicDatabase 的 綁定。
Hilt 模塊還需要添加 @InstallIn 注解,用來表示這些信息在哪些依賴項容器或者組件中可用。但是什么是組件?我們來介紹更多細節(jié)。
Hilt 組件
組件是 Hilt 生成的一個類,負責提供類型的實例,就像我們手動實現(xiàn)的容器一樣。在編譯期,Hilt 遍歷依賴關(guān)系圖,并生成代碼,來提供所有類型并攜帶它們的傳遞依賴項。

Hilt 為絕大多數(shù) Android 框架類生成組件 (或稱為依賴項容器)。每個組件關(guān)聯(lián)信息 (或稱為綁定) 通過組件層次結(jié)構(gòu)向下傳遞。

如果 MusicDatabase 的綁定在 SingletonComponent (對應 Application 類) 中是可用的,那么綁定在其他組件中也可用。
當您在 Android 框架類上添加 @AndroidEntryPoint 注解時,Hilt 將在編譯期自動生成組件,并完成組件的創(chuàng)建、管理以及關(guān)聯(lián)到與之對應的類中。
模塊的 @InstallIn 注解用于控制這些綁定的可用位置,以及它們可以使用哪些其他綁定。
限定作用域
回到手動創(chuàng)建 PlayActivityContainer 的代碼中,您是否意識到一個問題?每次需要 MusicDatabase 依賴項時,我們都會創(chuàng)建一個不同的實例。
class PlayActivityContainer(val context: Context) {
fun provideMusicDatabase(): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
}
fun provideMusicPlayer() = MusicPlayer(
provideMusicDatabase()
)
}
這并不是我們想要的,因為我們可能希望在整個應用中重用相同的 MusicDatabase 實例。我們可以通過持有一個變量來共享相同的實例,而不是一個函數(shù)。
class PlayActivityContainer {
val musicDatabase: MusicDatabase =
Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
fun provideMusicPlayer() = MusicPlayer(musicDatabase)
}
基本上我們會將 MusicDatabase 類型的作用域限定到該容器中,因為我們總是會提供相同的實例作為依賴項。如何通過 Hilt 來實現(xiàn)這一點呢?好吧,毫無疑問,使用另一個注解!
在添加了 @Provides 注解的方法上,我們可以通過使用 @Singleton 注解來告訴 Hilt 組件總是共享該類型的相同實例。
@Module
@InstallIn(SingletonComponent::class)
object DataModule {
@Singleton
@Provides
fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
}
}
@Singleton 是一個作用域注解。每一個 Hilt 組件都有與之關(guān)聯(lián)的作用域注解。

如果您想要限定一個類型的作用域為 ActivityComponent,您需要使用 ActivityScoped 注解。這些注解不僅可以在模塊中使用,還可以添加到類上,前提是該類的構(gòu)造方法已經(jīng)被添加 @Inject 注解。
綁定
有兩種類型的綁定:
-
未限定作用域綁定 : 沒有添加作用域注解的綁定,例如
MusicPlayer,如果它們沒有被裝載到模塊中,則所有組件都可以使用這些綁定。 -
限定作用域綁定 : 添加了作用域注解的綁定,例如
MusicDatabase,以及被裝載到模塊中的未限定作用域綁定,只有對應組件及其組件層次結(jié)構(gòu)下方組件可以使用這些綁定。
Jetpack 擴展
Hilt 可以與最流行的 Jetpack 庫的集成使用: ViewModel、Navigation、Compose 以及 WorkManager。
除了 ViewModel,每個集成都需要在項目中添加不同的庫。獲取更多信息,請查閱: Hilt 和 Jetpack 集成。您還記得我們在文章開頭看到的 iosched 中的 FeedViewModel 代碼嗎?您想看看使用 Hilt 支持之后的效果嗎?
@HiltViewModel
class FeedViewModel @Inject constructor(
private val loadCurrentMomentUseCase: LoadCurrentMomentUseCase,
loadAnnouncementsUseCase: LoadAnnouncementsUseCase,
private val loadStarredAndReservedSessionsUseCase: LoadStarredAndReservedSessionsUseCase,
getTimeZoneUseCase: GetTimeZoneUseCase,
getConferenceStateUseCase: GetConferenceStateUseCase,
private val timeProvider: TimeProvider,
private val analyticsHelper: AnalyticsHelper,
private val signInViewModelDelegate: SignInViewModelDelegate,
themedActivityDelegate: ThemedActivityDelegate,
private val snackbarMessageManager: SnackbarMessageManager
) : ViewModel(),
FeedEventListener,
ThemedActivityDelegate by themedActivityDelegate,
SignInViewModelDelegate by signInViewModelDelegate {
/* ... */
}
為了讓 Hilt 知道如何提供該 ViewModel 的實例,我們不僅要在構(gòu)造函數(shù)上添加 @Inject 注解,還需要對這個類添加 @HiltViewModel 注解。
就是這樣,Hilt 會幫助您創(chuàng)建 ViewModel 的提供程序,您無需再手動處理。
了解更多
Hilt 基于另一個流行的依賴注入庫 Dagger 進行構(gòu)建!在接下來的文章中,Dagger 將會被頻繁提及!如果您正在使用 Dagger,Dagger 可以與 Hilt 配合使用,請查看我們之前的文章《從 Dagger 遷移到 Hilt 可帶來的收益》。有關(guān) Hilt 的更多信息,您可以參閱以下資源:
- Dagger 基礎(chǔ)知識
- 遷移到 Hilt 指南
- Hilt 及 Dagger 注解的區(qū)別及使用方式的備忘錄
- 使用 Hilt 實現(xiàn)依賴項注入
- Codelab: 在 Android 應用中使用 Hilt
以上是本文的全部內(nèi)容,我們即將推出更多 MAD Skills,敬請關(guān)注后續(xù)更新。
歡迎您 點擊這里 向我們提交反饋,或分享您喜歡的內(nèi)容、發(fā)現(xiàn)的問題。您的反饋對我們非常重要,感謝您的支持!