Hilt 介紹 | MAD Skills

本文是 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 生成的類,負責提供類型的實例

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

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

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

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

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容