依赖注入框架 Hilt 的使用

这篇记录下 Hilt 最基本的使用方法。

Hilt 是基于 Dagger 开发的依赖注入框架。我们知道 Dagger 是 Java 开发中无可否认的功能最为强大的依赖注入框架,但是它的缺点是使用起来比较复杂,尤其是对于初学者而言,学习曲线异常陡峭。而依赖注入框架在 Android 开发中又是一个非常重要的工具,因此,在 Kotlin 成为了 Android 开发者的首选语言之后,开源社区中诞生了许多基于 Kotlin 依赖注入框架,比如 KoinKodein。谷歌当然也注意到了这一点,所以也基于 Dagger 推出了更简单易上手的 Hilt,它的主要优势是:

  • 基于 Android 简化了 Dagger 相关的基础架构;
  • 提供了一组标准的 Component 和 Scope 以简化使用、提升可读性以及便于代码共享;
  • 简化针对不同的构建类型(比如测试、调试或发布类型)配置不同的绑定。

之所以能做到以上这几点,是因为 Hilt 自动帮我们做了很多工作,比如自动生成用于和 Android Framwork 绑定的 Component/Scoped annotations/Bindings/Qualifier 等,而如果使用 Dagger 的话,这些都是需要我们自己手动编写代码来管理的。

Hilt 的基本使用

使用方式上,Hilt 和 Dagger 相比最明显的区别是,不再需要定义 Component,对于每一个 Android 基础类,Hilt 会自动为它生成对应的 Component,并且会根据安卓组件的生命周期来创建和销毁。如下:

Hilt 组件 注入器面向的对象
ApplicationComponent Application
ActivityRetainedComponent ViewModel
ActivityComponent Activity
FragmentComponent Fragment
ViewComponent View
ViewWithFragmentComponent 带有 @WithFragmentBindings 注释的 View
ServiceComponent Service

因此,对于一个最简单的 MVVM 项目而言,使用 Hilt 做依赖注入一般需要按照以下的步骤:

  • 添加 Hilt 插件和依赖,对于每个 module 都要单独添加
  • 使用 @HiltAndroidApp 标注你自定义的 Application
  • 使用 @AndroidEntryPoint 标注你的 Fragment 或者 Activity
  • 使用 @HiltViewModel 标注你的 ViewModel 类,以及用 @Inject 标注构造器
  • 使用 @Inject 标注需要注入的依赖,比如 Repository 等
  • 定义 Hilt 模块,用于提供无法直接注入的依赖,比如接口类和外部的依赖类等
    • 使用 @InstallIn 标注该模块的作用范围
    • 使用 @Provides 标注提供每个依赖的方法
    • 使用 @Binds 标注需要注入依赖的接口和具体实现(通过抽象类和抽象方法)

具体例子可以参考我的开源小项目:Jithub

模块和组件

虽然 Hilt 大大简化了依赖注入的使用,但是使用方式上和 Dagger 并没有太大区别,最基础的组成部分依旧是模块和组件。对于某个 Hilt 模块,如果我们想要注入多个相同类型的依赖,同样需要通过定义限定符来实现。

使用注解定义限定符(例子来自官方文档):

1
2
3
4
5
6
7
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttpClient

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class OtherInterceptorOkHttpClient

除此之外,我们还可以使用 Hilt 自带的限定符,比如 @ActivityContext@ApplicationContext 等。

组件的使用细节

之前提到过对于不同的 Android 类,Hilt 会为之生成对应的组件,我们在模块中通过 @InstalledIn 引用组件,然后 Hilt 会将模块安装到对应的组件中,最后再把依赖注入到组件中。

除此之外,每个组件都有自己的生命周期,而且我们还可以为组件限定作用域。不同的组件作用域内可使用的依赖也不同,比如 @FragmentScoped 作用域内的依赖可以在 @ActivityScoped 的组件中使用,但是无法在 @ViewScoped 的组件中使用。

Android 类 生成的组件 作用域
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
带有 @WithFragmentBindings 注释的 View ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

其它细节

如果需要在 Hilt 不支持的类中注入依赖,比如 ContentProvider,我们可以通过创建 EntryPoint 来访问这些依赖。

通过 @EntryPoint 创建依赖入口(通常是一个接口),然后在其中定义方法提供所需要的依赖,再通过 @InstalledIn 来定义安装到哪个组件。另外,访问 EntryPoint 依赖也和普通的依赖不同,我们需要通过之前定义好的 EntryPoint 接口来访问依赖项。

1
val entryPointInterface = EntryPointAccessors.fromXxx(Application/Activity/Fragment/Context, EntryPointInterface::class.java)

EntryPointAccessors 中包含不同的创建方法,对应于创建不同的组件,比如 EntryPointAccessors.fromApplication() 对应于 SingletonComponentfromActivity() 对应于 ActivityComponent 等。创建 EntryPoint 接口对象之后,我们就可以通过它来访问依赖了。

小结

可以看到,Hilt 大大简化了传统依赖注入框架的使用方式,但是核心功能基本和 Dagger 保持一致,因此,开发人员可以将更多的注意力放在开发上,从而提升开发效率。