Hilt 介绍 | MAD Skills

本文是 MAD Skills 系列 中有关 Hilt 的第一篇文章!在本文中,我们将探讨依赖项注入 (DI) 对应用的重要性,以及 Jetpack 推荐的 Android DI 解决方案——Hilt。

如果您更喜欢通过视频了解此内容,可以在此处查看:

△ Hilt 及依赖项注入——MAD Skills

在 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,敬请关注后续更新。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!`1