什么是依赖注入
依赖项注入 (DI) 是一种广泛用于编程的技术,非常适用于 Android 开发。遵循 DI 的原则可以为构造良好的应用架构奠定基础。
类通常需要引用其他类。例如,Car 类可能需要引用 Engine 类。这些必需的类称为依赖项,在下面的示例中,Car 类必须拥有 Engine 类的一个实例才能运行。
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.start()
}
类可通过以下三种方式获取所需的对象:
- 由类构造其所需的依赖项。在上面的示例中,Car 将创建并初始化自己的 Engine 实例。
- 从其他地方抓取。某些 Android API(如 Context getter 和 getSystemService())便是如此获取对象的。
- 以参数形式提供。我们可以在声明类的构造或方法时将这些依赖项定义为参数。在上面的示例中,Car 构造函数将接收 Engine 作为参数。
第三种方式就是依赖项注入!使用这种方法,您可以获取并提供类的依赖项,而不必让类实例自行获取。
上面的代码示例并非依赖项注入的示例,将上面的代码进行改造为依赖项注入后如下:
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
在Android中是想依赖项注入的方式主要有两种:
- 构造函数注入。这就是上面描述的方式。您将某个类的依赖项传入其构造函数。
- 字段注入(或 setter 注入)。 某些 Android 框架类(如 activity 和 fragment)由系统实例化,因此无法进行构造函数注入。使用字段注入时,依赖项将在创建类后实例化。代码如下所示:
class Car {
lateinit var engine: Engine
fun start() {
engine.start()
}
}
fun main(args: Array) {
val car = Car()
car.engine = Engine()
car.start()
}
以上可见,我们已经在日常开发中无时无刻不在使用依赖注入,而Dagger或Hilt框架只是提供了让我们更加方便、高效、健壮的完成依赖注入的能力。
自动依赖项注入
在上一个示例中,我们没有以依赖于任何库而自行创建、提供并管理了不用类的依赖项。这称为手动依赖项注入或人工依赖项注入。在Car
示例中只有一个依赖项,但是实际上我们可能会有很多很多的依赖项。依赖项越多,手动注入就越繁琐,手动注入还会带来很多问题:
- 对于大型应用,获取所有依赖项并正确连接它们可能需要大量样板代码。在多层架构中,要为顶层创建一个对象,必须提供下层的所有依赖项。例如,要制造一辆真车,可能需要引擎、变速器、底盘以及其他部件;而要制造引擎,则需要汽缸和火花塞。
- 如果无法在传入依赖项之前构造依赖项(例如,当使用延迟初始化或将对象作用域限定为应用流时),则需要编写并维护用于管理内存中依赖项生命周期的自定义容器(或依赖关系图)。
有一些库通过自动执行创建和提供依赖项的过程解决此问题。它们归为两类:
- 基于反射的解决方案,可在运行时连接依赖项。
- 静态解决方案,可在编译时生成连接依赖项的代码。
Dagger就是适用于Java、Kotlin和Android的热门依赖项注入库,由Google进行维护。Dagger 会创建和管理依赖关系图,方便在应用中使用 DI。它提供了完全静态和编译时依赖项,解决了基于反射的解决方案(如Guice)的诸多开发和性能问题。
在Android中使用Hilt
官方定义:
Hilt是推荐用于在 Android 中实现依赖项注入的 Jetpack 库。Hilt 定义了一种在应用中实现 DI 的标准方法,它会为项目中的每个 Android 类提供容器并自动为您管理其生命周期。
Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 提供的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。
我的理解:
Hilt是为了解决Dagger在Android中使用非常麻烦、困难,而推出的专门用于在Android中更加方便、简单的使用Dagger的库。
Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。
Hilt & Dagger
Hilt 在依赖项注入库 Dagger 的基础上构建而成,提供了一种将 Dagger 纳入 Android 应用的标准方法。
关于 Dagger,Hilt 的目标如下:
- 简化 Android 应用的 Dagger 相关基础架构。
- 创建一组标准的组件和作用域,以简化设置、提高可读性以及在应用之间共享代码。
- 提供一种简单的方法来为各种 build 类型(如测试、调试或发布)配置不同的绑定。
由于 Android 操作系统会实例化它自己的许多框架类,因此在 Android 应用中使用 Dagger 要求您编写大量的样板。Hilt 可减少在 Android 应用中使用 Dagger 所涉及的样板代码。Hilt 会自动生成并提供以下各项:
- 用于将 Android 框架类与 Dagger 集成的组件 - 不必手动创建。
- 作用域注解 - 与 Hilt 自动生成的组件一起使用。
- 预定义的绑定 - 表示 Android 类,如 Application 或 Activity。
- 预定义的限定符 - 表示 @ApplicationContext 和 @ActivityContext。
Dagger 和 Hilt 代码可以共存于同一代码库中。不过,在大多数情况下,最好使用 Hilt 管理我们在 Android 上对 Dagger 的所有使用。
添加Gradle依赖项
首先,将 hilt-android-gradle-plugin 插件添加到项目的根级 build.gradle 文件中:
plugins {
...
id("com.google.dagger.hilt.android") version "2.56.2" apply false
}
然后,应用 Gradle 插件并在 app/build.gradle 文件中添加以下依赖项:
plugins {
id("com.google.devtools.ksp")
id("com.google.dagger.hilt.android")
}
android {
...
}
dependencies {
implementation("com.google.dagger:hilt-android:2.56.2")
ksp("com.google.dagger:hilt-android-compiler:2.56.2")
}
Hilt 使用 Java 8 功能。如需在项目中启用 Java 8,请将以下代码添加到 app/build.gradle
文件中:
android {
...
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
提示:是至少Java8而不是必须Java8,Java17也是可以的。
如果在添加依赖后编译报错:Plugin [id: 'com.google.devtools.ksp'] was not found in any of the following sources:
可以通过在项目根目录的build.gradle
中添加ksp插件解决:
plugins {
id("com.google.devtools.ksp") version "2.0.21-1.0.27" apply false
}
注意 :KSP 版本的前一部分必须与 build 中使用的 Kotlin 版本一致。例如,如果项目使用的是 Kotlin 2.0.21,则 KSP 版本必须是 2.0.21-x.y.z 版本之一。
Hilt应用(Application)类
所有使用Hilt的应用都必须包含一个带有@HiltAndroidApp
注解的Application
类,也就是说我们需要自定义Application
类并添加@HiltAndroidApp
注解。
@HiltAndroidApp
会触发Hilt的代码生成操作,生成的代码包括应用的一个基类,改基类充当应用级依赖项容器。
@HiltAndroidApp
class ExampleApplication : Application() { ... }
生成的这一 Hilt 组件会附加到 Application 对象的生命周期,并为其提供依赖项。此外,它也是应用的父组件,这意味着,其他组件可以访问它提供的依赖项。
将依赖项注入 Android 类
在Application
类中设置了 Hilt 且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint 注解的其他 Android 类提供依赖项:
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() { ... }
Hilt 目前支持以下 Android 类:
- Application(通过使用 @HiltAndroidApp)
- ViewModel(通过使用 @HiltViewModel)
- Activity
- Fragment
- View
- Service
- BroadcastReceiver
如果使用 @AndroidEntryPoint 为某个 Android 类添加注解,则还必须为依赖于该类的 Android 类添加注解。例如,如果为某个 fragment 添加注解,则还必须为使用该 fragment 的所有 activity 添加注解。
注意:在 Hilt 对 Android 类的支持方面适用以下几项例外情况:
- Hilt 仅支持继承
ComponentActivity
的 activity,如AppCompatActivity
。- Hilt 仅支持继承
androidx.Fragment
的 Fragment。- Hilt 不支持保留的 fragment。
@AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项。
使用@Inject
注解从组件获取依赖项进行字段注入到Android类:
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
@Inject lateinit var analytics: AnalyticsAdapter
...
}
注意:由 Hilt 注入的字段不能为私有字段。尝试使用 Hilt 注入私有字段会导致编译错误。
Hilt 注入的类可以有同样使用注入的其他基类。如果这些类是抽象类,则它们不需要 @AndroidEntryPoint
注解。
定义 Hilt 绑定
为了执行字段注入,Hilt 需要知道如何从相应组件提供必要依赖项的实例。“绑定”包含将某个类型的实例作为依赖项提供所需的信息。
向 Hilt 提供绑定信息的一种方法是构造函数注入。在某个类的构造函数中使用 @Inject 注解,以告知 Hilt 如何提供该类的实例:
data class Car @Inject constructor (private val engine: Engine) {
fun start() : String = engine.start()
}
在一个类的代码中,带有注解的构造函数的参数即是该类的依赖项。在本例中,Engine 是 Car 的一个依赖项。因此,Hilt 还必须知道如何提供 Engine 的实例。
提示:构造函数注入支持空参构造或所有参数的类型均提供了Hilt绑定定义。
Hilt 模块
定义Hilt绑定除了使用构造注入外,还提供了Hilt模块的方式来定义Hilt绑定。
Hilt模块是一个带有@Module
注解的类。与Dagger模块
一样,它会告知 Hilt 如何提供某些类型的实例。与 Dagger 模块不同的是,我们必须使用 @InstallIn 为 Hilt 模块添加注解,以告知 Hilt 每个模块将用在或安装在哪个 Android 类中。
在 Hilt 模块中提供的依赖项可以在生成的所有与 Hilt 模块安装到的 Android 类关联的组件中使用。
使用 @Binds 注入接口实例
以 Engine 为例。如果 Engine 是一个接口,则无法通过构造函数注入它,而应改在Hilt模块内创建一个带有@Binds
注解的抽象函数,以此向 Hilt 提供绑定信息。
data class Car @Inject constructor (private val engine: Engine) {
fun start() : String = engine.start()
}
interface Engine {
fun start(): String
}
//构造器注入,因为Hilt也需要知道如何提供BYDEngine的实例。
class BYDEngine @Inject constructor(): Engine{
override fun start() = "BYD engine has started!"
}
@Module
@InstallIn(ActivityComponent::class)
abstract class ActivityModule {
@Binds
abstract fun provideEngine(engine: BYDEngine): Engine
}
Hilt 模块 ActivityModule 带有 @InstallIn(ActivityComponent.class) 注解,因为您希望 Hilt 将该依赖项注入 ExampleActivity。此注解意味着,ExampleActivity 中的所有依赖项都可以在应用的所有 activity 中使用。
使用 @Provides 注入实例
接口不是无法通过构造函数注入类型的唯一一种情况。如果我们要注入的类,是一个外部库中的类,或则必须是有Builder模式才能创建实例的类,也无法通过构造函数注入。
带有@Providers
注解的函数会向Hilt提供一下信息:
- 函数返回类型会告知 Hilt 函数提供哪个类型的实例。
- 函数参数会告知 Hilt 相应类型的依赖项。
- 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。
data class Car @Inject constructor (private val engine: Engine) {
fun start() : String = engine.start()
}
interface Engine {
fun start(): String
}
//假设EngineImpl由三方库提供,所以不能使用构造注入。
class EngineImpl (private val brand: String): Engine{
override fun start() = "$brand engine has started!"
}
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {
@Provides
fun provideEngine(): Engine{
return EngineImpl("Benz")
}
}
注意:
@Provides
不能在抽象类或接口中使用,推荐在objcet
中使用。
为同一类型提供多个绑定
当我们希望为同一类型提供不同实现就必须向Hilt 提供多个绑定。我们可以使用限定符为同一类型定义多个绑定。
限定符是一种注解,当为某个类型定义了多个绑定时,我们可以使用它来标识该类型的特定绑定。
以上面的栗子为例,首先,定义要用于为 @Binds 或 @Provides 方法添加注解的限定符:
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BYD
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Benz
然后可以通过使用相应的限定符为字段或参数添加注解来注入所需的特定类型:
data class Car @Inject constructor (@Benz private val engine: Engine) {
fun start() : String = engine.start()
}
Hilt 中的预定义限定符
Hilt 提供了一些预定义的限定符。例如,由于您可能需要来自应用或 activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。
为 Android 类生成的组件
对于我们可以从中执行字段注入的每个 Android 类,都有一个关联的 Hilt 组件,我们可以在 @InstallIn 注解中引用该组件。每个 Hilt 组件负责将其绑定注入相应的 Android 类。
前面的示例演示了如何在 Hilt 模块中使用 ActivityComponent。
Hilt 提供了以下组件:
Hilt 组件 | 注入器面向的对象 |
---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
不适用 |
ViewModelComponent |
ViewModel |
ActivityComponent |
Activity |
FragmentComponent |
Fragment |
ViewComponent |
View |
ViewWithFragmentComponent |
带有 @WithFragmentBindings 注解的 View
|
ServiceComponent |
Service |
注意:Hilt 不会为广播接收器生成组件,因为 Hilt 直接从 SingletonComponent 注入广播接收器。
组件生命周期
Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。
生成的组件 | 创建时机 | 销毁时机 |
---|---|---|
SingletonComponent |
Application#onCreate() |
Application 已销毁 |
ActivityRetainedComponent |
Activity#onCreate() |
Activity#onDestroy() |
ViewModelComponent |
ViewModel 已创建 |
ViewModel 已销毁 |
ActivityComponent |
Activity#onCreate() |
Activity#onDestroy() |
FragmentComponent |
Fragment#onAttach() |
Fragment#onDestroy() |
ViewComponent |
View#super() |
View 已销毁 |
ViewWithFragmentComponent |
View#super() |
View 已销毁 |
ServiceComponent |
Service#onCreate() |
Service#onDestroy() |
注意:ActivityRetainedComponent 在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁。
组件作用域
默认情况下,Hilt 中的所有绑定都未限定作用域。这意味着,每当应用请求绑定时,Hilt 都会创建所需类型的一个新实例。
Hilt 允许将绑定的作用域限定为特定组件。Hilt 只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。
下表列出了生成的每个组件的作用域注解:
Android 类 | 生成的组件 | 作用域 |
---|---|---|
Application |
SingletonComponent |
@Singleton |
Activity |
ActivityRetainedComponent |
@ActivityRetainedScoped |
ViewModel |
ViewModelComponent |
@ViewModelScoped |
Activity |
ActivityComponent |
@ActivityScoped |
Fragment |
FragmentComponent |
@FragmentScoped |
View |
ViewComponent |
@ViewScoped |
带有 @WithFragmentBindings 注解的 View
|
ViewWithFragmentComponent |
@ViewScoped |
Service |
ServiceComponent |
@ServiceScoped |
在下面的栗子中,如果使用 @ActivityScoped 将 Car 的作用域限定为 ActivityComponent,Hilt 会在相应 activity 的整个生命周期内提供 Car 的同一实例:
@ActivityScoped
data class Car @Inject constructor (@Benz private val engine: Engine) {
fun start() : String = engine.start()
}
注意:将绑定的作用域限定为某个组件的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定。如果绑定的内部状态要求在某一作用域内使用同一实例,绑定需要同步,或者绑定的创建成本很高,那么将绑定的作用域限定为某个组件是一种恰当的做法。
组件默认绑定
每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入我们自己的自定义绑定。需要注意的是,这些绑定对应于常规 activity 和 fragment 类型,而不对应于任何特定子类。这是因为,Hilt 会使用单个 activity 组件定义来注入所有 activity。每个 activity 都有此组件的不同实例。
Android 组件 | 默认绑定 |
---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
Application |
ViewModelComponent |
SavedStateHandle |
ActivityComponent |
Application 和 Activity
|
FragmentComponent |
Application 、Activity 和 Fragment
|
ViewComponent |
Application 、Activity 和 View
|
ViewWithFragmentComponent |
Application 、Activity 、Fragment 、View
|
ServiceComponent |
Application 和 Service
|
在 Hilt 不支持的类中注入依赖项
Hilt 支持最常见的 Android 类。不过,有时候我们需要在 Hilt 不支持的类中执行字段注入。
在这些情况下,您可以使用 @EntryPoint 注解创建入口点。入口点是由 Hilt 管理的代码与并非由 Hilt 管理的代码之间的边界。它是代码首次进入 Hilt 所管理的对象图的位置。入口点允许 Hilt 使用并非由 Hilt 管理的代码提供依赖关系图中的依赖项。
下面看个栗子:
interface Engine {
fun start(): String
}
class BYDEngine @Inject constructor() : Engine {
override fun start() = "BYD engine has started!"
}
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AppEntryPoint {
fun provideBydEngine(): BYDEngine
}
class Car {
private val engine: Engine = EntryPointAccessors.fromApplication(HiltApp.applicationContext, AppEntryPoint::class.java).provideBydEngine()
fun start(): String {
return engine.start()
}
}
在上面的栗子中,必须使用ApplicationContext
检索入口点,因为入口点安装在SingletonComponent
中。如果您要检索的绑定位于ActivityComponent
中,应改用ActivityContext
。
使用Hilt注入ViewModel对象
提供 ViewModel
,方法是为其添加 @HiltViewModel
注解,并在 ViewModel
对象的构造函数中使用 @Inject
注解。
@HiltViewModel
class ExampleViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val repository: ExampleRepository
) : ViewModel() {
...
}
然后,带有 @AndroidEntryPoint
注解的 activity 或 fragment 可以使用 ViewModelProvider
或 by viewModels()
照常获取 ViewModel
实例:
@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
private val exampleViewModel: ExampleViewModel by viewModels()
...
}
@ViewModelScoped
所有 Hilt ViewModel 都由 ViewModelComponent 提供,遵循与 ViewModel 相同的生命周期,因此可以在配置更改后继续存在。如需将依赖项的作用域限定为 ViewModel,请使用@ViewModelScoped 注解。
如果需要在不同 ViewModel 之间共享单个实例,则应使用 @ActivityRetainedScoped 或 @Singleton 限定其作用域。