Kotlin 中使用 Hilt 的開發實踐

Hilt 是基於 Dagger 開發的全新的依賴項注入代碼庫,它簡化了 Android 應用中 Dagger 的調用方式。本文經過簡短的代碼片斷爲您展現其核心功能以幫助開發者們快速入門 Hilt。html

配置 Hilt

如需在應用中配置 Hilt,請先參考 Gradle Build Setupandroid

完成安裝所有的依賴和插件之後,僅需在您的 Application 類以前添加 @HiltAndroidApp 註解便可開始使用 Hilt,而無需其它操做。git

@HiltAndroidApp
class App : Application()

定義而且注入依賴項

當您寫代碼用到依賴項注入的時候,有兩個要點須要考慮:github

  1. 您須要注入依賴項的類;
  2. 能夠做爲依賴項進行注入的類。

而上述這兩點並不互斥,並且在不少狀況下,您的類既能夠注入依賴項同時也包含依賴。api

使依賴項可注入緩存

若是須要在 Hilt 中使某個類變得可注入,您須要告訴 Hilt 如何建立該類的實例。該過程叫作綁定 (bindings)。框架

在 Hilt 中定義綁定有三種方式:ide

  1. 在構造函數上添加 @Inject 註解;
  2. 在模塊上使用 @Binds 註解;
  3. 在模塊上使用 @Provides 註解。

⮕ 在構造函數上使用 @Inject 註解函數

任何類的構造函數均可以添加 @Inject 註解,這樣該類在整個工程中均可以做爲依賴進行注入。測試

class OatMilk @Inject constructor() {
  ...
  }

⮕ 使用模塊

在 Hilt 中另外兩種將類轉爲可注入的方法是使用模塊。

Hilt 模塊 就好像 "菜譜",它能夠告訴 Hilt 如何建立那些不具有構造函數的類的實例,好比接口或者系統服務。

此外,在您的測試中,任何模塊均可以被其它模塊所替代。這有利於使用 mock 替換接口實現。

模塊經過 @InstallIn 註解被安裝在特定的 Hilt 組件 中。這一部分我會在後面詳細介紹。

選項 1: 使用 @Binds 爲接口建立綁定

若是您但願在須要 Milk 時候,使用 OatMilk 在代碼中取而代之,那麼能夠在模塊中建立一個抽象方法,而後爲該方法添加 @Binds 註解。注意 OatMilk 自己必須是可注入的,僅需在 OatMilk 的構造函數上添加 @Inject 註解便可。

interface Milk { ... }

class OatMilk @Inject constructor(): Milk {
  ...
}

@Module
@InstallIn(ActivityComponent::class)
abstract class MilkModule {
  @Binds
  abstract fun bindMilk(oatMilk: OatMilk): Milk
}

選項 2: 使用 @Provides 來建立工廠函數

當實例沒法被直接建立,您能夠建立一個 provider。provider 就是能夠返回對象實例的工廠函數。

一個典型的例子就是系統服務,好比 ConnectivityManager,它們的實例須要經過 Context 對象來返回。

@Module
@InstallIn(ApplicationComponent::class)
object ConnectivityManagerModule {
  @Provides
  fun provideConnectivityManager(
    @ApplicationContext context: Context
  ) = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
}

只要使用註解 @ApplicationContext 或者 @ActivityContextContext 對象就是默承認注入的。

注入依賴

當依賴可注入後,您可使用 Hilt 經過兩種方式:

  1. 做爲構造函數的參數注入;
  2. 做爲字段注入。

⮕ 做爲構造函數參數注入

interface Milk { ... }
interface Coffee { ... }

class Latte @Inject constructor(
  private val Milk milk,
  private val Coffee coffee
) {
  ...
}

若是構造函數使用了註解 @Inject,Hilt 會根據您爲類型所定義的綁定來注入全部的參數。

⮕ 做爲字段注入

interface Milk { ... }
interface Coffee { ... }

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  @Inject lateinit var milk: Milk
  @Inject lateinit var coffee: Coffee

  ...
}

若是類是入口點,這裏特指使用了 @AndroidEntryPoint 註解的類 (後面章節會詳細介紹),那麼該類中全部包含 @Inject 註解的字段均會被注入。

使用 @Inject 註解的字段必須是 public 類型的。也能夠添加 lateinit 來避免字段空值,由於它們在注入以前的初始值就是 null

請注意做爲字段注入依賴項的場景僅僅適合類必須包含無參構造函數的狀況,好比 Activity。在大多數場景下,您更應經過構造函數的參數來注入依賴項。

其它重要的概念

入口點

還記得我在上文裏提到,在不少狀況下,您的類會在經過依賴注入建立的同時包含被注入的依賴項。有些狀況下,您的類可能不是經過依賴項注入來建立,可是仍然會被注入依賴項。一個典型的例子就是 activity,它是由 Android 框架內部建立的,而不是由 Hilt 建立。

這些類屬於 Hilt 依賴圖譜的 入口點,並且 Hilt 須要知道這些類包含要注入的依賴。

⮕ Android 入口點

大部分入口點是所謂的 Android 入口點:

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

若是是 Android 入口點,請添加 @AndroidEntryPoint 註解。

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  ...
}

⮕ 其它入口點

Android 入口點對於大多數應用已經足夠,可是若是您使用了不含有 Dagger 的庫或者還沒有在 Hilt 中支持的 Android 組件,那麼您可能須要建立您本身的入口點來手動訪問 Hilt 依賴圖譜。詳情請查看 將任意類轉換爲入口點

ViewModel

ViewModel 是一個特例: 由於框架會建立它們,它既不是被直接實例化的,也不是 Android 入口點。ViewModel 須要使用特殊的 @HiltViewModel 註解,當 ViewModel 經過 byViewModels() 建立的時候,該註解使 Hilt 可以向 ViewModel 注入依賴,和其它類的 @Inject 註解的原理類似。

interface Milk { ... }
interface Coffee { ... }

@HiltViewModel
class LatteViewModel @Inject constructor(
  private val milk: Milk,
  private val coffee: Coffee
) : ViewModel() {
  ...
}

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  private val viewModel: LatteViewModel by viewModels()
  ...
}

若是您須要訪問 ViewModel 已緩存的狀態,能夠添加 @Assisted 註解,將 SavedStateHandle 做爲構造函數參數進行注入。

@HiltViewModel
class LatteViewModel @Inject constructor(
  @Assisted private val savedState: SavedStateHandle,
  private val milk: Milk,
  private val coffee: Coffee
) : ViewModel() {
  ...
}

要使用 @ViewModelInject,您可能須要添加更多依賴。更多詳細內容請詳見 Hilt 和 Jetpack 集成指南

組件

各個模塊都是安裝在 Hilt 組件 中的,經過 @InstallIn(<組件名>) 指定。模塊的組件主要用於防止意外將依賴注入到錯誤的位置。好比,@InstallIn(ServiceComponent.class) 能夠防止註解所修飾的模塊中的 binding 和 provider 被 activity 調用。

此外,binding 的做用域會被限制在組件所屬的整個模塊。也就是接下來咱們要講的...

做用域

默認狀況下,綁定都未被限定做用域。正如上面的示例,意味着每次注入 Milk 的時候,您均可以得到一個新的 OatMilk 實例。若是添加了 @ActivityScoped 註解,那麼您會將綁定的做用域限制到 ActivityComponent

@Module
@InstallIn(ActivityComponent::class)
abstract class MilkModule {
  @ActivityScoped
  @Binds
  abstract fun bindMilk(oatMilk: OatMilk): Milk
}

如今您的模塊被限制做用域了,Hilt 在每一個 activity 實例中僅建立一個 OatMilk 實例。此外,OatMilk 實例會綁定到 activity 的生命週期中——當 activity 的 onCreate() 被調用的時候,它會被建立,而當 activity 的 onDestroy() 被調用的時候,它會被銷燬。

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  @Inject lateinit var milk: Milk
  @Inject lateinit var moreMilk: Milk //這裏的實例和上面的相同

  ...
}

在本例中,milkmoreMilk 指向同一個 OatMilk 實例。然而,若是您有多個 LatteActivity 實例,它們會包含各自的 OatMilk 實例。

相應的,其它被注入到該 activity 的依賴,它們的做用域是一致的。所以它們也會引用到相同的 OatMilk 實例:

// Milk 實例的建立會在 Fridge 存在以前,由於它被綁定到了 activity 的生命週期中
class Fridge @Inject constructor(private val Milk milk) { ... }

@AndroidEntryPoint
class LatteActivity : AppCompatActivity() {
  // 下面四項共享了同一個 Milk 實例
  @Inject lateinit var milk: Milk
  @Inject lateinit var moreMilk: Milk
  @Inject lateinit var fridge: Fridge
  @Inject lateinit var backupFridge: Fridge

  ...
}

做用域依賴於您的模塊所安裝的組件,好比 @ActivityScoped 僅僅用於在 ActivityComponent 安裝的模塊內的綁定。

做用域一樣決定了注入實例的生命週期: 在本例中,被 FridgeLatteActivity 使用的 Milk 的單獨實例會在 LatteActivityonCreate() 被調用的時候被建立——而當 onDestroy() 被調用的時候被銷燬。這也意味着當配置發生改變的時候,Milk 不會 "倖免",由於配置發生改變的時候會調用 activity 的 onDestroy()。您能夠經過使用生命週期更長的做用域來避免該問題,好比使用 @ActivityRetainedScope

若是想要了解可用的做用域列表、相關的組件以及所遵循的生命週期,請參見 Hilt 組件

Provider 注入

有些時候您但願可以更加直接地控制注入實例的建立。好比,您可能但願基於業務邏輯,注入某個類型的一個實例或者幾個實例。針對這樣的場景,您可使用 dagger.Provider:

class Spices @Inject constructor() { ... }

class Latte @Inject constructor(
  private val spiceProvider: Provider<Spices>
) {
  fun addSpices() {
    val spices = spiceProvider.get()// 建立 Spices 的新實例
    ...
  }
}

provider 注入能夠忽略具體的依賴類型以及注入的方式。任何可被注入的內容都可以封裝在 Provider<...> 中來使用 provider 注入的方式。

依賴注入框架 (像 Dagger 和 Guice) 一般被用於大型且複雜的項目。而 Hilt 既容易上手,配置起來又很是簡單,同時做爲獨立的代碼包,還兼顧了 Dagger 中可被各類類型應用,不管代碼規模大小,都可兼容的強大特性。

若是您但願瞭解更多關於 Hilt 的內容、它的工做原理,以及其它對您來講有用的特性,請移步官方網站,瞭解更多詳細的介紹和參考文檔

相關文章
相關標籤/搜索