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 的内容、它的工做原理,以及其它对您来讲有用的特性,请移步官方网站,了解更多详细的介绍和参考文档

相关文章
相关标签/搜索