本文是 MAD Skills 系列 中有關(guān) Hilt 的第一篇文章捏题!在本文中玻褪,我們將探討依賴項(xiàng)注入 (DI) 對應(yīng)用的重要性,以及 Jetpack 推薦的 Android DI 解決方案——Hilt公荧。
如果您更喜歡通過視頻了解此內(nèi)容带射,可以 點(diǎn)擊這里 查看。
在 Android 應(yīng)用中循狰,您可以通過遵循依賴項(xiàng)注入的原則窟社,為良好的應(yīng)用架構(gòu)奠定基礎(chǔ)。這有助于重用代碼晤揣、易于重構(gòu)桥爽、易于測試!更多關(guān)于 DI 的好處昧识,請參閱: Android 中的依賴項(xiàng)注入。
在項(xiàng)目中創(chuàng)建類的實(shí)例時盗扒,您可以通過提供及傳遞所需依賴項(xiàng)跪楞,手動處理依賴關(guān)系圖。
但是每次都手動執(zhí)行會增加模版代碼并且容易出錯侣灶。以 iosched 項(xiàng)目 (Google I/O 開源應(yīng)用) 中的一個 ViewModel 為例甸祭,您能想象創(chuàng)建一個 FeedViewModel 所需的依賴項(xiàng)及傳遞依賴項(xiàng)需要多大的代碼量嗎?
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 {
/* ... */
}
這是復(fù)雜且機(jī)械化的褥影,并且我們很容易弄錯依賴關(guān)系池户。依賴項(xiàng)注入庫可以讓我們利用 DI 的優(yōu)勢,而無需手動提供依賴關(guān)系凡怎,因?yàn)閹鞎湍伤行枰拇a校焦。這也就是 Hilt 發(fā)揮作用的地方。
Hilt
Hilt 是一個由 Google 開發(fā)的依賴項(xiàng)注入庫统倒,它通過處理復(fù)雜的依賴關(guān)系并為您生成原本需要手動編寫的模版代碼寨典,幫助您在應(yīng)用中充分利用 DI 的最佳實(shí)踐。
Hilt 通過使用注解在編譯期幫您生成代碼房匆,來保證運(yùn)行時性能耸成。這是利用 JVM DI 庫 Dagger 的能力實(shí)現(xiàn)的报亩,而 Hilt 是基于 Dagger 構(gòu)建的。
Hilt 是 Jetpack 推薦的 Android 應(yīng)用 DI 解決方案井氢,它附帶工具并且支持其他 Jetpack 庫弦追。
快速開始
所有使用 Hilt 的應(yīng)用都必須包含被 @HiltAndroidApp
注解的 Application 類,它會在編譯期觸發(fā) Hilt 的代碼生成花竞。為了 Hilt 能將依賴項(xiàng)注入到 Activity 中骗卜,Activity 需要使用 @AndroidEntryPoint
注解。
@HiltAndroidApp
class MusicApp : Application()
@AndroidEntryPoint
class PlayActivity : AppCompatActivity() { /* ... */ }
注入一個依賴項(xiàng)時左胞,需要在您希望注入的變量上添加 @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
類型的實(shí)例呢躺枕?還需要額外的工作服猪!我們還需要告訴 Hilt 如何處理,當(dāng)然還是使用注解拐云!
在類的構(gòu)造方法上添加 @Inject
注解罢猪,告訴 Hilt 怎樣創(chuàng)建該類的實(shí)例。
class MusicPlayer @Inject constructor() {
fun play(id: String) { ... }
}
這就是將依賴項(xiàng)注入到 Activity 中所需的全部內(nèi)容叉瘩!非常簡單膳帕!我們從一個簡單的例子開始,因?yàn)?MusicPlayer 并不依賴任何其他類型薇缅。但是如果我們將其他依賴作為參數(shù)傳遞危彩,Hilt 會在提供 MusicPlayer 的實(shí)例時處理并滿足這些依賴項(xiàng)。
實(shí)際上泳桦,這是一個非常簡單初級的例子汤徽。但是如果您必須手動完成我們上述工作,您會怎樣做灸撰?
手動實(shí)現(xiàn)
手動執(zhí)行 DI 時谒府,您需要一個依賴項(xiàng)容器,它負(fù)責(zé)提供類型的實(shí)例并管理這些實(shí)例的生命周期浮毯。簡單的說完疫,這些就是 Hilt 在幕后所做的內(nèi)容。
當(dāng)我們在 Activity 上添加 @AndroidEntryPoint
注解時亲轨,Hilt 會自動創(chuàng)建一個依賴項(xiàng)容器趋惨,并管理、關(guān)聯(lián)到 PlayActivity
上惦蚊。這里我們手動實(shí)現(xiàn) PlayActivityContainer
容器器虾。通過在 MusicPlayer
上添加 @Inject
注解讯嫂,等同于告訴容器如何提供 MusicPlayer
的實(shí)例。
// PlayActivity 已被添加 @AndroidEntryPoint 注解
class PlayActivityContainer {
// MusicPlayer 已被添加 @Inject 注解
fun provideMusicPlayer() = MusicPlayer()
}
在 Activity 中兆沙,我們需要創(chuàng)建一個容器實(shí)例欧芽,并使用它對 Activity 的依賴項(xiàng)賦值。對于 Hilt 而言葛圃,在 Activity 上添加 @AndroidEntryPoint
注解時也完成了容器實(shí)例的創(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)看見库正,當(dāng) @Inject
注解被添加到類的構(gòu)造函數(shù)上時曲楚,它會告訴 Hilt 如何提供該類的實(shí)例。當(dāng)變量被添加 @Inject
注解褥符,并且變量所屬的類被添加 @AndroidEntryPoint
注解時龙誊,Hilt 會向該類中注入一個相應(yīng)類型的實(shí)例。
@AndroidEntryPoint
注解可以添加到絕大部分 Android 框架類上喷楣,不僅僅是 Activity趟大。它會為被添加注解的類去創(chuàng)建一個依賴項(xiàng)容器的實(shí)例,并填充所有添加了 @Inject
注解的變量铣焊。
在 Application
類上添加 @HiltAndroidApp
注解逊朽,除了觸發(fā) Hilt 生成代碼之外,還創(chuàng)建了一個與 Application 關(guān)聯(lián)的依賴項(xiàng)容器曲伊。
Hilt 模塊
我們既然已經(jīng)了解了 Hilt 基礎(chǔ)叽讳,那一起來提高示例的復(fù)雜性吧。現(xiàn)在熊昌,MusicPlayer
的構(gòu)造函數(shù)中绽榛,需要一個依賴項(xiàng) MusicDatabase
。
class MusicPlayer @Inject constructor(
private val db: MusicDatabase
) {
fun play(id: String) { ... }
}
因此婿屹,我們需要告訴 Hilt 如何提供 MusicDatabase
實(shí)例。當(dāng)類型是一個接口推溃,或者您無法在構(gòu)造函數(shù)上添加 @Inject
昂利,例如類來自于您無法修改的庫。
假設(shè)我們在應(yīng)用中 使用 Room 作為持久性存儲庫铁坎》浼椋回到我們手動實(shí)現(xiàn) PlayActivityContainer
的場景中,當(dāng)我們通過 Room 提供 MusicDatabase
時硬萍,這將是一個抽象類扩所,我們希望在提供依賴項(xiàng)時執(zhí)行一些代碼。接下來朴乖,當(dāng)提供 MusicPlayer
的實(shí)例時祖屏,我們需要調(diào)用提供或者滿足 MusicDatabase
依賴項(xiàng)的方法助赞。
class PlayActivityContainer(val context: Context) {
fun provideMusicDatabase(): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
}
fun provideMusicPlayer() = MusicPlayer(
provideMusicDatabase()
)
}
在 Hilt 中我們無需擔(dān)心傳遞依賴,因?yàn)樗鼤詣雨P(guān)聯(lián)所有需要傳遞的依賴項(xiàng)袁勺。然而雹食,我們需要讓 Hilt 知道如何提供 MusicDatabase
類型的實(shí)例。為此期丰,我們使用 Hilt 模塊群叶。
Hilt 模塊是一個被添加了 @Module
注解的類。在該類中钝荡,我們可以實(shí)現(xiàn)函數(shù)來告訴 Hilt 如何提供確切類型的實(shí)例街立。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í)例埠通。函數(shù)體包含 Hilt 需要執(zhí)行的代碼塊帆竹,這與我們手動實(shí)現(xiàn)完全一致亿眠。
返回類型 MusicDatabase
告知 Hilt 此函數(shù)提供什么類型。函數(shù)的參數(shù)告訴 Hilt 該類型所需的依賴項(xiàng)。本案例中翠桦,ApplicationContext
已經(jīng)在 Hilt 中可用。這段代碼告知 Hilt 如何提供 MusicDatabase
類型的實(shí)例抒巢,換句話說伐脖,我們已經(jīng)有了一個 MusicDatabase
的 綁定。
Hilt 模塊還需要添加 @InstallIn
注解喷鸽,用來表示這些信息在哪些依賴項(xiàng)容器或者組件中可用众雷。但是什么是組件?我們來介紹更多細(xì)節(jié)做祝。
Hilt 組件
組件是 Hilt 生成的一個類砾省,負(fù)責(zé)提供類型的實(shí)例,就像我們手動實(shí)現(xiàn)的容器一樣混槐。在編譯期编兄,Hilt 遍歷依賴關(guān)系圖,并生成代碼声登,來提供所有類型并攜帶它們的傳遞依賴項(xiàng)狠鸳。
Hilt 為絕大多數(shù) Android 框架類生成組件 (或稱為依賴項(xiàng)容器)悯嗓。每個組件關(guān)聯(lián)信息 (或稱為綁定) 通過組件層次結(jié)構(gòu)向下傳遞件舵。
如果 MusicDatabase
的綁定在 SingletonComponent
(對應(yīng) Application
類) 中是可用的,那么綁定在其他組件中也可用脯厨。
當(dāng)您在 Android 框架類上添加 @AndroidEntryPoint
注解時铅祸,Hilt 將在編譯期自動生成組件,并完成組件的創(chuàng)建合武、管理以及關(guān)聯(lián)到與之對應(yīng)的類中临梗。
模塊的 @InstallIn
注解用于控制這些綁定的可用位置涡扼,以及它們可以使用哪些其他綁定。
限定作用域
回到手動創(chuàng)建 PlayActivityContainer
的代碼中夜焦,您是否意識到一個問題壳澳?每次需要 MusicDatabase
依賴項(xiàng)時,我們都會創(chuàng)建一個不同的實(shí)例茫经。
class PlayActivityContainer(val context: Context) {
fun provideMusicDatabase(): MusicDatabase {
return Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
}
fun provideMusicPlayer() = MusicPlayer(
provideMusicDatabase()
)
}
這并不是我們想要的巷波,因?yàn)槲覀兛赡芟M谡麄€應(yīng)用中重用相同的 MusicDatabase
實(shí)例。我們可以通過持有一個變量來共享相同的實(shí)例卸伞,而不是一個函數(shù)抹镊。
class PlayActivityContainer {
val musicDatabase: MusicDatabase =
Room.databaseBuilder(
context, MusicDatabase::class.java, "music.db"
).build()
fun provideMusicPlayer() = MusicPlayer(musicDatabase)
}
基本上我們會將 MusicDatabase
類型的作用域限定到該容器中,因?yàn)槲覀兛偸菚峁┫嗤膶?shí)例作為依賴項(xiàng)荤傲。如何通過 Hilt 來實(shí)現(xiàn)這一點(diǎn)呢垮耳?好吧,毫無疑問遂黍,使用另一個注解终佛!
在添加了 @Provides
注解的方法上,我們可以通過使用 @Singleton
注解來告訴 Hilt 組件總是共享該類型的相同實(shí)例雾家。
@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)的作用域注解。
如果您想要限定一個類型的作用域?yàn)?ActivityComponent
芯咧,您需要使用 ActivityScoped
注解牙捉。這些注解不僅可以在模塊中使用,還可以添加到類上敬飒,前提是該類的構(gòu)造方法已經(jīng)被添加 @Inject
注解邪铲。
綁定
有兩種類型的綁定:
-
未限定作用域綁定 : 沒有添加作用域注解的綁定,例如
MusicPlayer
无拗,如果它們沒有被裝載到模塊中带到,則所有組件都可以使用這些綁定。 -
限定作用域綁定 : 添加了作用域注解的綁定英染,例如
MusicDatabase
阴孟,以及被裝載到模塊中的未限定作用域綁定,只有對應(yīng)組件及其組件層次結(jié)構(gòu)下方組件可以使用這些綁定税迷。
Jetpack 擴(kuò)展
Hilt 可以與最流行的 Jetpack 庫的集成使用: ViewModel、Navigation锹漱、Compose 以及 WorkManager箭养。
除了 ViewModel,每個集成都需要在項(xiàng)目中添加不同的庫哥牍。獲取更多信息毕泌,請查閱: 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 的實(shí)例挠说,我們不僅要在構(gòu)造函數(shù)上添加 @Inject
注解,還需要對這個類添加 @HiltViewModel
注解愿题。
就是這樣损俭,Hilt 會幫助您創(chuàng)建 ViewModel 的提供程序,您無需再手動處理潘酗。
了解更多
Hilt 基于另一個流行的依賴注入庫 Dagger 進(jìn)行構(gòu)建杆兵!在接下來的文章中,Dagger 將會被頻繁提及仔夺!如果您正在使用 Dagger琐脏,Dagger 可以與 Hilt 配合使用,請查看我們之前的文章《從 Dagger 遷移到 Hilt 可帶來的收益》缸兔。有關(guān) Hilt 的更多信息日裙,您可以參閱以下資源:
- Dagger 基礎(chǔ)知識
- 遷移到 Hilt 指南
- Hilt 及 Dagger 注解的區(qū)別及使用方式的備忘錄
- 使用 Hilt 實(shí)現(xiàn)依賴項(xiàng)注入
- Codelab: 在 Android 應(yīng)用中使用 Hilt
以上是本文的全部內(nèi)容,我們即將推出更多 MAD Skills惰蜜,敬請關(guān)注后續(xù)更新昂拂。
歡迎您 點(diǎn)擊這里 向我們提交反饋,或分享您喜歡的內(nèi)容蝎抽、發(fā)現(xiàn)的問題政钟。您的反饋對我們非常重要,感謝您的支持樟结!