一、为什么使用Hilt
Hilt
是Android的依赖注入库。回答为什么要使用Hilt
这个问题其实就是回答:什么是依赖注入库?为什么要使用依赖注入库?Hilt
解决了什么问题?
什么是依赖注入?
依赖注入即Dependency Injection
,简称DI
。Hilt
、Dagger
、Koin
等都是依赖注入库。依赖注入最主要是帮助代码解耦和测试。
类通常需要引用其它类,比如Car
类可能需要引用Engine
类,这些必需类称为依赖项。
类获取依赖项的方式有三种:
- 类构造其所需的依赖项
- 从其他地方抓取,例如
Context
getter
和getSystemService()
- 以参数的形式提供,这种方式也称依赖注入,基于控制反转原则。在构造类时提供这些依赖项,或者将这些依赖项传入需要各个依赖项的函数。
class Car {
private val engine = Engine()
fun start() {
engine.start()
}
}
复制代码
上述示例在Car
类中构造了Engine
对象实例,这可能会有问题:
Car
与Engine
关系过于紧密。Car
的实例使用一种类型的Engine
,并且无法轻松使用子类或替代实现。如果Car
要构造自己的Engine
,您必须创建两种类型的Car
,而不是直接将同一Car
重用于Gas
和Electric
类型的引擎。- 对
Engine
的强依赖使得测试更加困难。
如果使用依赖注入
class Car(private val engine: Engine) {
fun start() {
engine.start()
}
}
fun main(args: Array) {
val engine = Engine()
val car = Car(engine)
car.start()
}
复制代码
Car
依赖于 Engine
,因此应用会创建 Engine
的实例,然后使用它构造 Car
的实例。
- 重用
Car
。可以将Engine
的不同实现传入Car
。 - 轻松测试
Car
。您可以传入测试替身以测试不同场景。
依赖注入库的作用
Android
中有两种主要的依赖项注入方式:
-
构造函数注入
如前述示例所示,将某个类的依赖项传入其构造函数
-
字段注入
某些
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
是适用于 Java
、Kotlin
和 Android
的热门依赖项注入库,由 Google
进行维护。Dagger
为您创建和管理依赖关系图,从而便于您在应用中使用 DI
。它提供了完全静态和编译时依赖项,解决了基于反射的解决方案(如 Guice)的诸多开发和性能问题。
为什么使用Hilt
依赖注入库
Hilt
在热门 DI
库 Dagger 的基础上构建而成,因而能够受益于 Dagger
的编译时正确性、运行时性能、可伸缩性和 Android Studio
的支持。Hilt
通过为项目中的每个 Android
类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI
(依赖项注入)的标准方法。
简单来说,就是门槛较低且好用,还有官方加持。
二、基本使用
使用Hilt
依赖注入库,首要引入组件相关依赖。
Hilt
的使用依赖引入与配置参见官方文档,这里不再赘述
2.1 Hilt组件
介绍Hilt
常用注解的使用前,先看一下Hilt的组件。
Hilt
组件层级
Hilt
提供了一组内置的组件,自动集成到Android
应用程序的各种生命周期中。
组件上方的注解是作用域注解,作用域绑定到该组件的生命周期。子组件的绑定可以依赖于父类组件中的任何绑定。
在
@InstallIn
模块中定义绑定时,绑定的范围必须与组件的范围匹配。例如,@InstallIn(ActivityComponent.class)
模块内的绑定只能使用@ActivityScoped
.
Hilt
组件注入
当使用Hilt
的@AndroidEntryPoint
注入自己的Android
类时,Hilt
组件被作为注入器,确定哪些绑定对该Android
类可见
组件 | 注入器面向的对象 |
---|---|
SingletonComponent |
Application |
ViewModelComponent |
ViewModel |
ActivityComponent |
Activity |
FragmentComponent |
Fragment |
ViewComponent |
View |
ViewWithFragmentComponent |
View with @WithFragmentBindings |
ServiceComponent |
Service |
注意:
Hilt
没有为broadcast receivers
提供组件,因为Hilt
直接从SignletonComponent
注入broadcast receivers
。
Hilt
组件生命周期
组件的生命周期通常受限于 Android
类的相应实例的创建和销毁
组件 | 范围 | 创建于 | 销毁于 |
---|---|---|---|
SingletonComponent |
@Singleton |
Application#onCreate() |
Application#onDestroy() |
ActivityRetainedComponent |
@ActivityRetainedScoped |
Activity#onCreate() 1 |
Activity#onDestroy() 1 |
ViewModelComponent |
@ViewModelScoped |
ViewModel 创建 |
ViewModel 毁坏 |
ActivityComponent |
@ActivityScoped |
Activity#onCreate() |
Activity#onDestroy() |
FragmentComponent |
@FragmentScoped |
Fragment#onAttach() |
Fragment#onDestroy() |
ViewComponent |
@ViewScoped |
View#super() |
View 毁坏 |
ViewWithFragmentComponent |
@ViewScoped |
View#super() |
View 毁坏 |
ServiceComponent |
@ServiceScoped |
Service#onCreate() |
Service#onDestroy() |
默认情况,Hilt
中的所有绑定都未限定作用,即每当应用请求绑定时,Hilt
都会创建所需类型的一个新实例。Hilt
允许将绑定的作用域限定为特定组件,Hilt
只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。
@ActivityScoped
class AnalyticsAdapter @Inject constructor(
private val service: AnalyticsService
) { ... }
复制代码
使用 @ActivityScoped
将 AnalyticsAdapter
的作用域限定为 ActivityComponent
,Hilt
会在相应 Activity
的整个生命周期内提供 AnalyticsAdapter
的同一实例。
注意:
将绑定的作用域限定为某个组件的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定
如果绑定的内部状态要求在某一作用域内使用同一实例,或者绑定的创建成本很高,那么将绑定的作用域限定为某个组件是一种恰当的做法。
每个 Hilt
组件都带有一组默认绑定,可以作为依赖项注入到您自己的自定义绑定中。
组件 | 默认绑定 |
---|---|
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 |
ActivityRetainedComponent
存在于配置更改中,因此它是在第一个onCreate
和最后一个onDestroy
时创建的
2.2 应用与入口点
Hilt
常用注解包含 @HiltAndroidApp
、@AndroidEntryPoint
、@Inject
、@Module
、@InstallIn
、@Provides
、@EntryPoint
等等。
应用@HiltAndroidApp
每个Android
程序中,都会有一个Application
,可以自定义或系统默认。在Hilt
,必须自定义一个Application
,否则Hilt
将无法正常工作。
@HiltAndroidApp
class MyApplication : Application() {
}
复制代码
@HiltAndroidApp
注解将会触发 Hilt
代码的生成,作为应用程序依赖项容器的基类,其生成的Hilt
组件依附于Application
的生命周期
入口点 @AndroidEntryPoint
在 Application
中设置好 @HiltAndroidApp
之后,可以使用@AndroidEntryPoint
在Android
类中启用成员注入。@AndroidEntryPoint
会为项目中的每个 Android
类生成一个单独的 Hilt
组件。这些组件可以从它们各自的父类接收依赖项。
可使用@AndroidEntryPoint
的类型为:
Activity
Fragment
View
Service
BroadcastReceiver
ViewModel
通过独立的Api@HiltViewModel
提供支持
使用@AndroidEntryPoint
为某个Android
类添加注解时,必须为依赖于该类的Android
类也添加注解。例如为Fragment
添加了注解,必须为使用该Fragment
的所有Activity
添加注解,否则会抛异常
java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
复制代码
看一下使用示例
class Truck @Inject constructor(){
fun deliver() {
println("deliver banner 者文公众号:者文静斋")
}
}
复制代码
@AndroidEntryPoint
class HiltFirstActivity:AppCompatActivity() {
private lateinit var mBinding:ActivityHiltFirstBinding
@Inject
lateinit var mTruck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityHiltFirstBinding.inflate(layoutInflater)
setContentView(mBinding.root)
mTruck.deliver()
}
}
复制代码
其中Activity
仅支持ComponentActivity
的子类,例如FragmentActivity
、AppCompatActivity
。
Fragment
仅支持继承 androidx.Fragment
的 Fragment
。
实例注入@Inject
@Inject
用于告知Hilt
如何提供该类的实例,常用于构造函数,非私有字段,方法中。尤其注意需要注入的字段不能声明为private
。
class Truck @Inject constructor(){
fun deliver() {
println("deliver banner 者文公众号:者文静斋")
}
}
复制代码
在类的构造函数中使用@Inject
注解,告知Hilt
如何提供该类实例。
如果构造函数有参数
class Truck @Inject constructor(val driver: Driver) {
fun deliver() {
println("Truck is delivering cargo. Driven by $driver")
}
}
复制代码
class Driver @Inject constructor()
复制代码
一个类中,带有注解的构造函数的参数即为该类的依赖项。
前述Driver
是Truck
的一个依赖项,则Hilt
需要指导如何提供Driver
的实例。只有Truck
的构造函数中所依赖的所有其他对象都支持依赖注入,则Truck
才可以被依赖注入
2.3 Hilt模块
有时,类型不能通过构造函数注入。例如,您不能通过构造函数注入接口,无法通过构造函数注入不归你所有的类型,例如外部库的类。在这些情况下,可以使用 Hilt
模块向 Hilt
提供绑定信息。
Hilt
模块是一个带有@Module
注解的类,它会告知Hilt
如何提供某些类型的实例。必须使用@InstallIn
注解为Hilt
模块添加注释,告知Hilt
每个模块将用在或安装在哪个Android
类中。
2.3.1 @Binds注入接口实例
接口没有构造函数,无法通过构造函数注入,则应向Hilt
提供绑定信息,即在Hilt
模块内创建一个带@Binds
注解的抽象函数,告知Hilt
在需要提供接口实例的时候使用哪种实现。
带@Binds
注解的函数应向Hilt
提供如下信息:
- 函数返回类型:告知
Hilt
函数提供哪个接口的实例 - 函数参数:告知
Hilt
要提供哪种实现
看一下代码示例
interface Engine {
fun start()
fun shutdown()
}
复制代码
先通过依赖注入的方式实现该接口的实现类
class GasEngine @Inject constructor() : Engine {
...
}
class ElectricEngine @Inject constructor() : Engine {
...
}
复制代码
然后,新建一个抽象类,在这个模块中提供Engine
接口需要的实例
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
}
复制代码
- 在
EngineModule
的上方声明一个@Module
注解,表示这一个用于提供依赖注入实例的模块@InstallIn
注解,用于确定 将模块安装到哪个Hilt 组件中。- 定义为抽象函数是因为不需要具体的函数体。函数名无所谓
- 示例中抽象函数的返回值必须是
Engine
,表示用于给Engine
类型接口提供实例;提供的实例由抽象函数的接收参数决定
最后,在Truck
类中注入实例
class Truck @Inject constructor(val driver: Driver) {
@Inject
lateinit var engine: Engine
fun deliver() {
engine.start()
println("Truck is delivering cargo. Driven by $driver")
engine.shutdown()
}
}
复制代码
@Module
@Module
常用于创建依赖类的对象(例如第三方库 OkHttp
、Retrofit
等等)和接口实例注入。使用 @Module
注解的类,需要使用 @InstallIn
注解指定 module
的范围。
@InstallIn
使用 @Module
注入的类,需要使用 @InstallIn
注解指定 module
的范围。例如使用 @InstallIn(ActivityComponent::class)
注解的 module
会绑定到 activity
的生命周期上,也意味着该Module
中所有依赖项都可以在应用的所有Activity
中使用。
@Binds
@Binds
注解告诉 Hilt
需要提供接口实例时使用哪个实现,需要在方法参数里面明确指明接口的实现类。
2.3.2 @Provides注入第三方类实例
如果某个类不归你所有或者必须使用构建器模式创建实例(例如Retrofit
、OkHttp
、Room
等),则无法通过构造函数注入,则可以主动告知Hilt
如何提供此类型的实例,即在Hilt
模块内创建一个函数,并使用@Provides
注解为该函数添加注释。
带@Provides
注解的函数可向Hilt
提供以下信息:
- 函数返回类型:告知
Hilt
函数提供哪个类型的实例 - 函数参数:告知
Hilt
相应类型的依赖项 - 函数主体:告知
Hilt
如何提供相应类型的实例。需要提供该类型实例时,Hilt
会执行该函数主体
以OKHttpClinet
为例
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
}
复制代码
函数名无所谓,示例中返回值必须为
OkHttpClient
,因为要提供OkHttpClient
类型实例函数体中,按照常规写法创建
OkHttpClient
实例即可
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var okHttpClient: OkHttpClient
...
}
复制代码
如果希望在 NetworkModule
中给 Retrofit
类型提供实例,而在创建 Retrofit
实例的时候,我们又可以选择让其依赖 OkHttpClient
@Module
@InstallIn(ActivityComponent::class)
class NetworkModule {
...
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://zhewendev.github.io/")
.client(okHttpClient)
.build()
}
}
复制代码
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var retrofit: Retrofit
...
}
复制代码
在Hilt
组件部分已解释,默认情况,Hilt
中的所有绑定都未限定作用,即每当应用请求绑定时,Hilt
都会创建所需类型的一个新实例。Retrofit
和 OkHttpClient
的实例理论上全局只需要一份,可以借助@Singleton
注解更改这一行为
@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
@Singleton
@Provides
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.build()
}
@Singleton
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("https://zhewendev.github.io/")
.client(okHttpClient)
.build()
}
}
复制代码
2.3.3 为同一类型提供多个绑定
如果您需要让 Hilt
以依赖项的形式提供同一类型的不同实现,必须向 Hilt
提供多个绑定。
前述的Engine
接口实现类有两个,通过EngineModule
中的bindEngine()
函数为Engine
接口提供实例,这个实例要么是GasEngine
,要么是ElectricEngine
,如何实现同时为一个接口提供两中不同的实例。解决这一问题就需要限定符。
@Qualifier
限定符是一种注释,当为某个类型定义了多个绑定时,您可以使用它来标识该类型的特定绑定。
@Qualifier
注解用于给相同类型的类或接口注入不同的实例
看一下代码示例
先定义要用于为@Binds
或@Provides
方法添加注释的限定符
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine
复制代码
@Retention
:用于声明注解的作用范围。选择
AnnotationRetention.BINARY
表示该注解在编译之后会得到保留,但是无法通过反射去访问这个注解。
然后,Hilt
需要知道如何提供与每个限定符对应的类型的实例
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@BindGasEngine
@Binds
abstract fun bindGasEngine(gasEngine: GasEngine): Engine
@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
复制代码
最后,获取相应的类型实例
class Truck @Inject constructor(val driver: Driver) {
@BindGasEngine
@Inject
lateinit var gasEngine: Engine
@BindElectricEngine
@Inject
lateinit var electricEngine: Engine
fun deliver() {
gasEngine.start()
electricEngine.start()
println("Truck is delivering cargo. Driven by $driver")
gasEngine.shutdown()
electricEngine.shutdown()
}
}
复制代码
示例定义了gasEngine
和electricEngine
两个字段,类型都是 Engine
。但是在 gasEngine
的上方,使用了 @BindGasEngine
注解,这样 Hilt
就会给它注入 GasEngine
的实例;electricEngine
同理。
2.3.4 预定义限定符
Android
开发中很多地方需要依赖于 Context
,如果我们想要依赖注入的类,它又是依赖于 Context
的,这个情况要如何解决呢?
@Singleton
class Driver @Inject constructor(val context: Context) {
}
复制代码
这里编译项目会报错,因为不知道如何提供context这个参数。
Android
提供了一些预定义Qualifier
,专门用于给我们提供Context
类型的依赖注入实例
@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}
复制代码
这种写法,Hilt
会自动提供一个Application
类型的Context
给到Truck
类当中
如果需要Activity
类型Context
,Hilt
还预置了另外一种 Qualifier
,我们使用 @ActivityContext
即可
@Singleton
class Driver @Inject constructor(@ActivityContext val context: Context) {
}
复制代码
这里编译会报错,Driver
是 Singleton
的,也就是全局都可以使用,但是却依赖了一个 Activity
类型的 Context
,这很明显是不可能的。将其注解改为@ActivityScoped
、@FragmentScoped
、@ViewScoped,
或者直接删掉都可以,这样再次编译就不会报错了。
对于 Application
和 Activity
这两个类型,Hilt
也是给它们预置好了注入功能。也就是说,如果你的某个类依赖于 Application
或者 Activity
,不需要想办法为这两个类提供依赖注入的实例,Hilt
自动就能识别它们
class Driver @Inject constructor(val application: Application) {
}
class Driver @Inject constructor(val activity: Activity) {
}
复制代码
注意必须是
Application
和Activity
这两个类型,即使是声明它们的子类型,编译都无法通过。
如果在自定义的MyApplication
中提供一些全局通用函数,导致很多地方都要依赖于写的MyApplication
,而MyApplication
又不被HIlt
识别,这时候可以做个向下类型转换就可以
@Module
@InstallIn(SingletonComponent::class)
class ApplicationModule {
@Provides
fun provideMyApplication(application: Application): MyApplication {
return application as MyApplication
}
}
复制代码
接下来,你在Truck类中可以这样声明依赖
class Driver @Inject constructor(val application: MyApplication) {
}
复制代码
2.4 ViewModel依赖注入
普通方式
对于ViewModel
,可以使用Hilt
普通的方式来实现实例注入
比如有一个Repository
类表示仓库层
class Repository @Inject constructor() {
...
}
复制代码
然后有一个MyViewModel
继承自ViewModel
,用于表示ViewModel
层
@ActivityRetainedScoped
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
...
}
复制代码
然后在MainActivity
中通过依赖注入的方式得到MyViewModel
实例
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MyViewModel
...
}
复制代码
这种用法field
不能是 private
类型;同时要加上 lateinit
,即稍后初始化
通过这种方式注入ViewModel
,只是一个普通对象,在Activity销毁时也会被回收,无法做到资源配置发生变更时依旧保存
独立依赖注入
对于 ViewModel
这种常用 Jetpack
组件,Hilt
专门为其提供了一种独立的依赖注入方式
ViewModel
实例对象的注入需要使用@HiltViewModel
注解
@HiltViewModel
class FooViewModel @Inject constructor(
val handle: SavedStateHandle,
val foo: Foo
) : ViewModel
复制代码
@AndroidEntryPoint
class MyActivity : AppCompatActivity() {
private val fooViewModel: FooViewModel by viewModels()
}
复制代码
2.5 不支持类中注入依赖
Hilt
支持最常见的 Android
类。不过有时可能需要在 Hilt
不支持的类中执行字段注入。
Hilt
一共支持6个入口点,Hilt
支持的入口少了一个关键的Android
组件:ContentProvider
,主要原因是 ContentProvider
的生命周期问题。
ContentProvider
的生命周期比较特殊,它在 Application
的 onCreate()
方法之前就能得到执行,而 Hilt
的工作原理是从 Application
的 onCreate()
方法中开始的,也就是说在这个方法执行之前,Hilt
的所有功能都还无法正常工作。
在这些情况下,可以使用 @EntryPoint
注释创建入口点。
首先,可以在ContentProvider
中自定义一个自己的入口点,然后在其中定义好要依赖注入的类型
class MyContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun getRetrofit(): Retrofit
}
...
}
复制代码
在 MyEntryPoint
中定义了一个 getRetrofit()
函数,并且函数的返回类型就是 Retrofit
。而Retrofit
是我们已支持依赖注入的类型。
如果我们想要在 MyContentProvider
的某个函数中获取 Retrofit 的实例,只需
class MyContentProvider : ContentProvider() {
...
override fun query(...): Cursor {
context?.let {
val appContext = it.applicationContext
val entryPoint = EntryPointAccessors.fromApplication(appContext, MyEntryPoint::class.java)
val retrofit = entryPoint.getRetrofit()
}
...
}
}
复制代码
借助 EntryPointAccessors
类,我们调用其 fromApplication()
函数来获得自定义入口点的实例,然后再调用入口点中定义的 getRetrofit()
函数就能得到 Retrofit
的实例了
三、Hilt工作原理
关于Hilt的工作原理,可以参看Hilt 工作原理 | MAD Skills,这里不再展开和赘述