Kotlin 中使用 Hilt 的开发实践

image

Hilt 是基于 Dagger 开发的全新的依赖项注入代码库,它简化了 Android 应用中 Dagger 的调用方式。本文通过简短的代码片段为您展示其核心功能以帮助开发者们快速入门 Hilt。

配置 Hilt

如需在应用中配置 Hilt,请先参考 Gradle Build Setup

完成安装全部的依赖和插件以后,仅需在您的 Application 类之前添加 @HiltAndroidApp 注解即可开始使用 Hilt,而无需其它操作。

@HiltAndroidApp
class App : Application()

定义并且注入依赖项

当您写代码用到依赖项注入的时候,有两个要点需要考虑:

  1. 您需要注入依赖项的类;
  2. 可以作为依赖项进行注入的类。

而上述这两点并不互斥,而且在很多情况下,您的类既可以注入依赖项同时也包含依赖。

使依赖项可注入

如果需要在 Hilt 中使某个类变得可注入,您需要告诉 Hilt 如何创建该类的实例。该过程叫做绑定 (bindings)。

在 Hilt 中定义绑定有三种方式:

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,634评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,951评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,427评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,770评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,835评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,799评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,768评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,544评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,979评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,271评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,427评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,121评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,756评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,375评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,579评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,410评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,315评论 2 352

推荐阅读更多精彩内容

  • Hilt是什么? Hilt是Android的依赖注入库,可以减少在项目中执行手动依赖项注入的样板代码。执行手动依赖...
    王灵阅读 4,669评论 0 8
  • [TOC] 1 Hilt相较于Dagger的优势 在我们了解Hilt之前,先需要知道Dagger, Dagger是...
    kevinsEegets阅读 7,074评论 0 4
  • Hilt 是什么 Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手...
    如沙雨下阅读 831评论 0 2
  • 表情是什么,我认为表情就是表现出来的情绪。表情可以传达很多信息。高兴了当然就笑了,难过就哭了。两者是相互影响密不可...
    Persistenc_6aea阅读 124,811评论 2 7
  • 16宿命:用概率思维提高你的胜算 以前的我是风险厌恶者,不喜欢去冒险,但是人生放弃了冒险,也就放弃了无数的可能。 ...
    yichen大刀阅读 6,044评论 0 4