Hilt 介紹 | MAD Skills

Android開發者發表於2021-10-19

本文是 MAD Skills 系列 中有關 Hilt 的第一篇文章!在本文中,我們將探討依賴項注入 (DI) 對應用的重要性,以及 Jetpack 推薦的 Android DI 解決方案——Hilt。

如果您更喜歡通過視訊瞭解此內容,可以 點選這裡 檢視。

在 Android 應用中,您可以通過遵循依賴項注入的原則,為良好的應用架構奠定基礎。這有助於重用程式碼、易於重構、易於測試!更多關於 DI 的好處,請參閱: Android 中的依賴項注入

在專案中建立類的例項時,您可以通過提供及傳遞所需依賴項,手動處理依賴關係圖。

但是每次都手動執行會增加模版程式碼並且容易出錯。以 iosched 專案 (Google I/O 開源應用) 中的一個 ViewModel 為例,您能想象建立一個 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 {
    /* ... */
}

這是複雜且機械化的,並且我們很容易弄錯依賴關係。依賴項注入庫可以讓我們利用 DI 的優勢,而無需手動提供依賴關係,因為庫會幫您生成所有需要的程式碼。這也就是 Hilt 發揮作用的地方。

Hilt

Hilt 是一個由 Google 開發的依賴項注入庫,它通過處理複雜的依賴關係併為您生成原本需要手動編寫的模版程式碼,幫助您在應用中充分利用 DI 的最佳實踐。

Hilt 通過使用註解在編譯期幫您生成程式碼,來保證執行時效能。這是利用 JVM DI 庫 Dagger 的能力實現的,而 Hilt 是基於 Dagger 構建的。

Hilt 是 Jetpack 推薦的 Android 應用 DI 解決方案,它附帶工具並且支援其他 Jetpack 庫。

快速開始

所有使用 Hilt 的應用都必須包含被 @HiltAndroidApp 註解的 Application 類,它會在編譯期觸發 Hilt 的程式碼生成。為了 Hilt 能將依賴項注入到 Activity 中,Activity 需要使用 @AndroidEntryPoint 註解。

@HiltAndroidApp
class MusicApp : Application()

@AndroidEntryPoint
class PlayActivity : AppCompatActivity() { /* ... */ }

注入一個依賴項時,需要在您希望注入的變數上新增 @Inject 註解。super.onCreate 被呼叫後,所有 Hilt 注入的變數都將可用。

@AndroidEntryPoint
class PlayActivity : AppCompatActivity() {

  @Inject lateinit var player: MusicPlayer

  override fun onCreate(savedInstanceState: Bundle) {
    super.onCreate(bundle)
    player.play("YHLQMDLG")
  }
}

在本案例中,我們向 PlayActivity 內注入 MusicPlayer,但是 Hilt 是如何知道怎樣提供 MusicPlayer 型別的例項呢?還需要額外的工作!我們還需要告訴 Hilt 如何處理,當然還是使用註解!

在類的構造方法上新增 @Inject 註解,告訴 Hilt 怎樣建立該類的例項。

class MusicPlayer @Inject constructor() {
  fun play(id: String) { ... }
}

這就是將依賴項注入到 Activity 中所需的全部內容!非常簡單!我們從一個簡單的例子開始,因為 MusicPlayer 並不依賴任何其他型別。但是如果我們將其他依賴作為引數傳遞,Hilt 會在提供 MusicPlayer 的例項時處理並滿足這些依賴項。

實際上,這是一個非常簡單初級的例子。但是如果您必須手動完成我們上述工作,您會怎樣做?

手動實現

手動執行 DI 時,您需要一個依賴項容器,它負責提供型別的例項並管理這些例項的生命週期。簡單的說,這些就是 Hilt 在幕後所做的內容。

當我們在 Activity 上新增 @AndroidEntryPoint 註解時,Hilt 會自動建立一個依賴項容器,並管理、關聯到 PlayActivity 上。這裡我們手動實現 PlayActivityContainer 容器。通過在 MusicPlayer上新增 @Inject 註解,等同於告訴容器如何提供 MusicPlayer 的例項。

// PlayActivity 已被新增 @AndroidEntryPoint 註解
class PlayActivityContainer {

  // MusicPlayer 已被新增 @Inject 註解
  fun provideMusicPlayer() = MusicPlayer()

}

在 Activity 中,我們需要建立一個容器例項,並使用它對 Activity 的依賴項賦值。對於 Hilt 而言,在 Activity 上新增 @AndroidEntryPoint 註解時也完成了容器例項的建立。

class PlayActivity : AppCompatActivity() {

  private lateinit var player: MusicPlayer

  // 在 Activity 上新增 @AndroidEntryPoint 註解時由 Hilt 建立
  private lateinit var container: PlayActivityContainer


  override fun onCreate(savedInstanceState: Bundle) {

    // @AndroidEntryPoint 同樣為您建立並填充欄位
    container = PlayActivityContainer()
    player = container.provideMusicPlayer()

    super.onCreate(bundle)
    player.play("YHLQMDLG")
  }
}

註解回顧

至此,我們已經看見,當 @Inject 註解被新增到類的建構函式上時,它會告訴 Hilt 如何提供該類的例項。當變數被新增 @Inject 註解,並且變數所屬的類被新增 @AndroidEntryPoint 註解時,Hilt 會向該類中注入一個相應型別的例項。

@AndroidEntryPoint 註解可以新增到絕大部分 Android 框架類上,不僅僅是 Activity。它會為被新增註解的類去建立一個依賴項容器的例項,並填充所有新增了 @Inject 註解的變數。

Application 類上新增 @HiltAndroidApp 註解,除了觸發 Hilt 生成程式碼之外,還建立了一個與 Application 關聯的依賴項容器。

Hilt 模組

我們既然已經瞭解了 Hilt 基礎,那一起來提高示例的複雜性吧。現在,MusicPlayer 的建構函式中,需要一個依賴項 MusicDatabase

class MusicPlayer @Inject constructor(
  private val db: MusicDatabase
) {
  fun play(id: String) { ... }
}

因此,我們需要告訴 Hilt 如何提供 MusicDatabase 例項。當型別是一個介面,或者您無法在建構函式上新增 @Inject,例如類來自於您無法修改的庫。

假設我們在應用中 使用 Room 作為永續性儲存庫。回到我們手動實現 PlayActivityContainer 的場景中,當我們通過 Room 提供 MusicDatabase 時,這將是一個抽象類,我們希望在提供依賴項時執行一些程式碼。接下來,當提供 MusicPlayer 的例項時,我們需要呼叫提供或者滿足 MusicDatabase 依賴項的方法。

class PlayActivityContainer(val context: Context) {

  fun provideMusicDatabase(): MusicDatabase {
    return Room.databaseBuilder(
              context, MusicDatabase::class.java, "music.db"
           ).build()
  }

  fun provideMusicPlayer() = MusicPlayer(
    provideMusicDatabase()
  )
}

在 Hilt 中我們無需擔心傳遞依賴,因為它會自動關聯所有需要傳遞的依賴項。然而,我們需要讓 Hilt 知道如何提供 MusicDatabase 型別的例項。為此,我們使用 Hilt 模組。

Hilt 模組是一個被新增了 @Module 註解的類。在該類中,我們可以實現函式來告訴 Hilt 如何提供確切型別的例項。Hilt 已知的此類資訊在行業內也被稱為繫結。

@Module
@InstallIn(SingletonComponent::class)
object DataModule {

  @Provides
  fun provideMusicDB(@ApplicationContext context: Context): MusicDatabase {
    return Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()
  }
}

在該函式上新增 @Provides 註解用來告訴 Hilt 如何提供 MusicDatabase 型別的例項。函式體包含 Hilt 需要執行的程式碼塊,這與我們手動實現完全一致。

返回型別 MusicDatabase 告知 Hilt 此函式提供什麼型別。函式的引數告訴 Hilt 該型別所需的依賴項。本案例中,ApplicationContext 已經在 Hilt 中可用。這段程式碼告知 Hilt 如何提供 MusicDatabase 型別的例項,換句話說,我們已經有了一個 MusicDatabase繫結

Hilt 模組還需要新增 @InstallIn 註解,用來表示這些資訊在哪些依賴項容器或者元件中可用。但是什麼是元件?我們來介紹更多細節。

Hilt 元件

元件是 Hilt 生成的一個類,負責提供型別的例項,就像我們手動實現的容器一樣。在編譯期,Hilt 遍歷依賴關係圖,並生成程式碼,來提供所有型別並攜帶它們的傳遞依賴項。

△ 元件是一個 Hilt 生成的類,負責提供型別的例項

△ 元件是一個 Hilt 生成的類,負責提供型別的例項

Hilt 為絕大多數 Android 框架類生成元件 (或稱為依賴項容器)。每個元件關聯資訊 (或稱為繫結) 通過元件層次結構向下傳遞。

△ Hilt 的元件層次結構

△ Hilt 的元件層次結構

如果 MusicDatabase 的繫結在 SingletonComponent (對應 Application 類) 中是可用的,那麼繫結在其他元件中也可用。

當您在 Android 框架類上新增 @AndroidEntryPoint 註解時,Hilt 將在編譯期自動生成元件,並完成元件的建立、管理以及關聯到與之對應的類中。

模組的 @InstallIn 註解用於控制這些繫結的可用位置,以及它們可以使用哪些其他繫結。

限定作用域

回到手動建立 PlayActivityContainer 的程式碼中,您是否意識到一個問題?每次需要 MusicDatabase 依賴項時,我們都會建立一個不同的例項。

class PlayActivityContainer(val context: Context) {

  fun provideMusicDatabase(): MusicDatabase {
    return Room.databaseBuilder(
              context, MusicDatabase::class.java, "music.db"
           ).build()
  }

  fun provideMusicPlayer() = MusicPlayer(
    provideMusicDatabase()
  )
}

這並不是我們想要的,因為我們可能希望在整個應用中重用相同的 MusicDatabase 例項。我們可以通過持有一個變數來共享相同的例項,而不是一個函式。

class PlayActivityContainer {

  val musicDatabase: MusicDatabase =
    Room.databaseBuilder(
      context, MusicDatabase::class.java, "music.db"
    ).build()

  fun provideMusicPlayer() = MusicPlayer(musicDatabase)
}

基本上我們會將 MusicDatabase 型別的作用域限定到該容器中,因為我們總是會提供相同的例項作為依賴項。如何通過 Hilt 來實現這一點呢?好吧,毫無疑問,使用另一個註解!

在新增了 @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 元件都有與之關聯的作用域註解。

△ 不同 Hilt 元件的作用域註解

△ 不同 Hilt 元件的作用域註解

如果您想要限定一個型別的作用域為 ActivityComponent,您需要使用 ActivityScoped 註解。這些註解不僅可以在模組中使用,還可以新增到類上,前提是該類的構造方法已經被新增 @Inject 註解。

繫結

有兩種型別的繫結:

  • 未限定作用域繫結 : 沒有新增作用域註解的繫結,例如 MusicPlayer,如果它們沒有被裝載到模組中,則所有元件都可以使用這些繫結。
  • 限定作用域繫結 : 新增了作用域註解的繫結,例如 MusicDatabase,以及被裝載到模組中的未限定作用域繫結,只有對應元件及其元件層次結構下方元件可以使用這些繫結。

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 的例項,我們不僅要在建構函式上新增 @Inject 註解,還需要對這個類新增 @HiltViewModel 註解。

就是這樣,Hilt 會幫助您建立 ViewModel 的提供程式,您無需再手動處理。

瞭解更多

Hilt 基於另一個流行的依賴注入庫 Dagger 進行構建!在接下來的文章中,Dagger 將會被頻繁提及!如果您正在使用 Dagger,Dagger 可以與 Hilt 配合使用,請檢視我們之前的文章《從 Dagger 遷移到 Hilt 可帶來的收益》。有關 Hilt 的更多資訊,您可以參閱以下資源:

以上是本文的全部內容,我們即將推出更多 MAD Skills,敬請關注後續更新。

歡迎您 點選這裡 向我們提交反饋,或分享您喜歡的內容、發現的問題。您的反饋對我們非常重要,感謝您的支援!