Hilt 介紹 | MAD Skills

本文是 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 生成的類,負(fù)責(zé)提供類型的實(shí)例

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

△ Hilt 的組件層次結(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)的作用域注解。

△ 不同 Hilt 組件的作用域注解

如果您想要限定一個類型的作用域?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 的更多信息日裙,您可以參閱以下資源:

以上是本文的全部內(nèi)容,我們即將推出更多 MAD Skills惰蜜,敬請關(guān)注后續(xù)更新昂拂。

歡迎您 點(diǎn)擊這里 向我們提交反饋,或分享您喜歡的內(nèi)容蝎抽、發(fā)現(xiàn)的問題政钟。您的反饋對我們非常重要,感謝您的支持樟结!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末养交,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瓢宦,更是在濱河造成了極大的恐慌碎连,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驮履,死亡現(xiàn)場離奇詭異鱼辙,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)玫镐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門倒戏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人恐似,你說我怎么就攤上這事杜跷。” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵葛闷,是天一觀的道長憋槐。 經(jīng)常有香客問我,道長淑趾,這世上最難降的妖魔是什么阳仔? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮扣泊,結(jié)果婚禮上近范,老公的妹妹穿的比我還像新娘。我一直安慰自己旷赖,他們只是感情好顺又,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著等孵,像睡著了一般稚照。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上俯萌,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天果录,我揣著相機(jī)與錄音,去河邊找鬼咐熙。 笑死弱恒,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的棋恼。 我是一名探鬼主播返弹,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼爪飘!你這毒婦竟也來了义起?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤师崎,失蹤者是張志新(化名)和其女友劉穎默终,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體犁罩,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡齐蔽,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了床估。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片含滴。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖丐巫,靈堂內(nèi)的尸體忽然破棺而出蛙吏,到底是詐尸還是另有隱情源哩,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布鸦做,位于F島的核電站,受9級特大地震影響谓着,放射性物質(zhì)發(fā)生泄漏泼诱。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一赊锚、第九天 我趴在偏房一處隱蔽的房頂上張望治筒。 院中可真熱鬧,春花似錦舷蒲、人聲如沸耸袜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堤框。三九已至,卻和暖如春纵柿,著一層夾襖步出監(jiān)牢的瞬間蜈抓,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工昂儒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沟使,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓渊跋,卻偏偏與公主長得像腊嗡,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子拾酝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容