Hilt 介绍 | MAD Skills_android

本文是 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 介绍 | MAD Skills_java_02


△ 组件是一个 Hilt 生成的类,负责提供类型的实例


Hilt 为绝大多数 Android 框架类生成组件 (或称为依赖项容器)。每个组件关联信息 (或称为绑定) 通过组件层次结构向下传递。

Hilt 介绍 | MAD Skills_kotlin_03


△ 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 介绍 | MAD Skills_dagger_04


△ 不同 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 的更多信息,您可以参阅以下资源:


  • Dagger 基础知识
  • 迁移到 Hilt 指南
  • Hilt 及 Dagger 注解的区别及使用方式的备忘录
  • 使用 Hilt 实现依赖项注入
  • Codelab: 在 Android 应用中使用 Hilt

以上是本文的全部内容,我们即将推出更多 MAD Skills,敬请关注后续更新。