Jetpack - Hilt
- 依赖注入、依赖注入框架
- Android 常用的依赖注入框架
- Hilt 的简单使用
1. 依赖注入、依赖注入框架
1.1 依赖注入
依赖注入的英文名是 Dependency Injection,简称 DI。其作用一言以蔽之:解耦。
举个栗子:
有一家卡车配送公司,只有一辆卡车用来送货。接到一个配送订单,客户委托配送两台电脑。可编写如下代码:
// 定义一个卡车 Truck,卡车有一个 deliver() 函数用于执行配送任务
// 在 deliver() 函数中先把两台电脑装上卡车,再进行配送
class Truck {
private val computer1 = Computer()
private val computer2 = Computer()
fun driver() {
loadToTruck(computer1)
loadToTruck(computer2)
beginToDeliver()
}
}
上面在 Truck
类中创建了两台电脑的实例,然后再对它们进行配送。卡车既要会送货,也要会生产电脑,使得卡车和电脑这两样不相干的东西耦合到一起去了,造成耦合度过高。
若又接到一个新的订单,去配送手机,那这辆卡车还要会生产手机才行。若增加配送蔬果的订单,那么这辆卡车还要会种地。。。最后发现,这已经就不是一辆卡车了,而是一个商品制造中心了:
其实,卡车并不需要关心配送的货物具体是什么,它的任务只需负责送货。即卡车是依赖于货物的,给了卡车货物,它就去送货,不给卡车货物,它就待命,修改代码如下:
// 在Truck类中添加了货物cargos字段,卡车是依赖于货物的
class Truck {
lateinit var cargos: List<Cargo>
fun driver() {
for(cargo in cargos) {
loadToTruck(cargo)
}
beginToDeliver()
}
}
这样,卡车不再关心任何商品制造的事情,而是依赖了什么货物,就去配送什么货物,只做本职应该做的事情。
这种让外部帮卡车初始化需要配送的货物的写法,就称之为:依赖注入。即让外部帮你初始化你的依赖,就叫依赖注入。
1.2 依赖注入框架
目前 Truck
类设计得比较合理了,但还存在问题。
若此时身份变成了一家电脑公司老板,该如何让一辆卡车来帮忙运送电脑呢?也许会很自然的写出如下代码:
class ComputerCompany {
private val computer1 = Computer()
private val computer2 = Computer()
fun deliverByTruck() {
val truck = Truck()
truck.cargos = listof(computer1, computer2)
truck.deliver()
}
}
上面代码同样也存在高耦合度问题:在 deliverByTruck()
函数中,为了让卡车送货,自己制造了一辆卡车。这明显是不合理的,电脑公司应该只负责生产电脑,它不应该去生产卡车。
更加合理的做法是,让卡车配送公司派辆空闲的卡车过来(就不用自己造车了),当卡车到达后,再将电脑装上卡车,然后执行配送任务即可。如下:
使用这种设计结构,就有很好的扩展性。若现在又有一家蔬果公司需要找一辆卡车来送菜,就完全可以使用同样的结构来完成任务,如下:
上图中呼叫卡车公司并让他们安排空闲车辆的这个部分,其实可以通过自己手写来实现,也可借助一些依赖注入框架来简化这个过程。
因此,依赖注入框架的作用就是为了替换下图所示的部分:
2. Android常用的依赖注入框架
2.1 Dagger
由Square公司开源,基于Java反射去实现的,从而有两个潜在的隐患:
反射是比较耗时的,用这种方式会降低程序的运行效率。(这问题不大,现在的程序中到处都在用反射)
依赖注入框架的用法总体来说比较有难度,很难一次性编写正确。而基于反射实现的依赖注入功能,在编译期无法得知依赖注入的用法是否正确,只能在运行时通过程序是否崩溃来判断。这样测试的效率低下,容易将一些 bug 隐藏得很深。
2.2 Dagger2
由 Google 开发,基于 Java 注解实现的,把 Dagger1 反射的那些弊端解决了:
通过注解,Dagger2 会在编译时期自动生成用于依赖注入的代码,不会增加任何运行耗时。另外,Dagger2 会在编译时检查依赖注入用法是否正确,若不正确则会直接编译失败,从而将问题尽可能早地抛出。即项目正常编译通过,说明依赖注入的用法基本没问题了。
但 Dagger2 使用比较复杂,若不能很好地使用它,可能会拖累你的项目,甚至会将一些简单的项目过度设计。
2.3 Hilt
Google 发布了 Hilt,是在依赖项注入库 Dagger 的基础上构建而成,一个专门面向 Android 的依赖注入框架。
相比于 Dagger2,Hilt 最明显的特征就是: 简单、提供了 Android 专属的 API。
3. Hilt 的简单使用
3.1 引入Hilt
第一步,在项目根目录的 build.gradle
文件中配置 Hilt 的插件路径:
buildscript {
...
dependencies {
...
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.31.1-alpha'
}
}
接下来,在 app/build.gradle
文件中,引入 Hilt 的插件并添加 Hilt 的依赖库:
plugins {
...
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
dependencies {
// hilt
implementation "com.google.dagger:hilt-android:2.31.1-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.31.1-alpha"
}
最后,Hilt 使用 Java 8 功能。如需启用 Java 8,在 app/build.gradle
文件中添加以下代码:
android {
...
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
这就成功将 Hilt 引入到项目当中了。
3.2 Hilt 的简单用法
在 Hilt 当中,必须要自定义一个 Application
才行,否则 Hilt 将无法正常工作。如下:
// 注解 @HiltAndroidApp 会触发 Hilt 的代码生成操作,
// 生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。
@HiltAndroidApp
class MyApplication: Application() {
}
在 Application
类中设置了 Hilt 且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint
注解的其他 Android 类提供依赖项。
Hilt 目前支持以下 Android 类:
Application(通过使用 @HiltAndroidApp)
Activity、Fragment、View、Service、BroadcastReceiver(通过使用 @AndroidEntryPoint)
以 Activity
为例,在 MainActivity
中进行依赖注入:
@AndroidEntryPoint
class MainActivity: AppCompatActivity() {
}
如把上面的 Truck
类注入到 MainActivity
当中:
@AndroidEntryPoint
class MainActivity: AppCompatActivity() {
// 步骤二:在 truck 字段的上方声明了一个 @Inject 注解
// 即希望通过 Hilt 来注入 truck 这个字段
@Inject
lateinit var truck: Truck
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
truck.driver()
}
}
// 步骤一:在 Truck 类的构造函数上声明了一个 @Inject 注解
// 即告诉 Hilt,可以通过这个构造函数来安排一辆卡车
class Truck @Inject constructor() {
fun driver() {
println("卡车运输货物")
}
}
这样在 MainActivity
中并没有去创建 Truck
的实例,只是用 @Inject
声明一下,就可以调用它的 deliver()
方法,即用 Hilt 完成了依赖注入的功能。
注:Hilt 注入的字段是不可以声明成 private 的。
3.3 带参数的依赖注入
比如在 Truck
类的构造函数中增加了一个 Driver
参数:
class Truck @Inject constructor(val driver: Driver) {
fun driver() {
println("卡车运输货物,司机是 $driver")
}
}
class Driver @Inject constructor() {
}
在 Driver
类的构造函数上声明了一个 @Inject
注解,这样 Driver
类就变成了无参构造函数的依赖注入方式。即 Truck
的构造函数中所依赖的所有其他对象都支持依赖注入了,那么 Truck
才可以被依赖注入。
3.4 接口的依赖注入
如定义个 Engine
接口和它的实现类如下:
interface Engine {
fun start()
fun shutdown()
}
class GasEngine @Inject constructor() : Engine {
override fun start() {
println("燃油车 start")
}
override fun shutdown() {
println("燃油车 shutdown")
}
}
接下来需要定义一个抽象类,使用 @Binds
注入接口实例 :
// 在 EngineModule 的上方声明一个 @Module 注解,表示这一个用于提供依赖注入实例的模块
// @InstallIn(ActivityComponent::class),表示把这个模块安装到Activity组件当中
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
// 1. 定义一个抽象函数(因为并不需实现具体的函数体)
// 2. 这个抽象函数的函数名叫什么都无所谓,也不会调用它。
// 3. 抽象函数的返回值必须是Engine,表示用于给Engine类型的接口提供实例。
// 4. 在抽象函数上方加上@Bind注解,这样Hilt才能识别它。
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
}
定义好抽象类 EngineModule
后,修改 Truck
类的代码如下:
class Truck @Inject constructor(val driver: Driver) {
@Inject
lateinit var engine: Engine
fun driver() {
engine.start()
println("卡车运输货物,司机是 $driver")
engine.shutdown()
}
}
这样,Hilt 就向 engine
字段注入了一个 GasEngine
的实例,也就完成了给接口进行依赖注入。
3.5 给相同类型注入不同的实例
比如再有个 Engine
接口的实现类:
class ElectricEngine @Inject constructor() : Engine {
override fun start() {
println("新能源车 start")
}
override fun shutdown() {
println("新能源车 shutdown")
}
}
此时,通过 EngineModule
中的 bindEngine()
函数为 Engine
接口提供实例,这个实例要么是 GasEngine
,要么是 ElectricEngine
,如何同时为一个接口提供两种不同的实例呢?
这时就要借助 Qualifier注解 来解决。Qualifier
注解的作用是给相同类型的类或接口注入不同的实例。
分别定义两个注解,如下:
// 注解的上方必须使用 @Qualifier 进行声明。
// 注解 @Retention,是用于声明注解的作用范围
// 选择 AnnotationRetention.BINARY 表示该注解在编译之后会得到保留,但无法通过反射去访问这个注解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindGasEngine
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BindElectricEngine
定义好上面两个注解后,把它们分别添加到 EngineModule
里对应的方法中:
@Module
@InstallIn(ActivityComponent::class)
abstract class EngineModule {
@BindGasEngine
@Binds
abstract fun bindEngine(gasEngine: GasEngine): Engine
@BindElectricEngine
@Binds
abstract fun bindElectricEngine(electricEngine: ElectricEngine): Engine
}
最后修改 Truck
类中的代码如下:
class Truck @Inject constructor(val driver: Driver) {
@BindGasEngine
@Inject
lateinit var gasEngine: Engine
@BindElectricEngine
@Inject
lateinit var electricEngine: Engine
fun driver() {
gasEngine.start()
electricEngine.start()
println("卡车运输货物,司机是 $driver")
gasEngine.shutdown()
electricEngine.shutdown()
}
}
这样就完成了给相同类型注入不同实例。
3.6 第三方类的依赖注入
给第三方类的依赖注入需要使用 @Provides
注解,如给 OkHttpClient
、Retrofit
类型提供实例如下:
@Module
@InstallIn(ActivityComponent::class)
class NetModule {
@Provides
fun provideOkHttpClient() : OkHttpClient {
return OkHttpClient.Builder()
.connectTimeout(0, TimeUnit.SECONDS)
.readTimeout(0, TimeUnit.SECONDS)
.writeTimeout(0, TimeUnit.SECONDS)
.build()
}
@Provides
fun provideRetrofit(okHttpClient: OkHttpClient) : Retrofit {
return Retrofit.Builder()
.addConverterFactory(GsonConverterFactory.create())
.baseUrl("http:xxx.com")
.client(okHttpClient)
.build()
}
}
在 provideOkHttpClient()
、provideRetrofit()
函数的上方加上 @Provides
注解,Hilt 就能识别它。
3.7 Hilt 内置组件和组件作用域
Hilt 一共内置了7种组件类型,分别用于注入到不同的场景,如 @InstallIn(ActivityComponent::class)
,就是把这个模块安装到 Activity
组件当中,如下表:
Hilt 一共提供了7种组件作用域注解,和上面的7个内置组件分别是一一对应的,如下表:
若想要在全程序范围内共用某个对象的实例,那么就使用 @Singleton
。
若想要在某个 Activity
,以及它内部包含的 Fragment
和 View
中共用某个对象的实例,那么就使用@ActivityScoped
。
以此类推。。。
作用域的包含关系如下:
即,对某个类声明了某种作用域注解之后,这个注解的箭头所能指到的地方,都可以对该类进行依赖注入,同时在该范围内共享同一个实例。
如 @Singleton
注解的箭头可以指向所有地方。
如 @ServiceScoped
注解的箭头无处可指,所以只能限定在 Service
自身当中使用。
如 @ActivityScoped
注解的箭头可以指向Fragment
、View
当中。
3.8 Hilt 中的预定义限定符
Hilt 提供了一些预定义的限定符。
例如,需要来自应用或 Activity
的 Context
类,就可以用 Hilt 提供的 @ApplicationContext
和 @ActivityContext
限定符。
用法很简单,只需要在 Context
参数前加上一个 @ApplicationContext
注解即可:
@Singleton
class Driver @Inject constructor(@ApplicationContext val context: Context) {
}
// 这边 @ApplicationContext 或 @ActivityContext 可以去掉,Hilt 也能识别
class Driver @Inject constructor(val application: Application) {}
class Driver @Inject constructor(val activity: Activity) {}
若要依赖于自己编写的 MyApplication
的,可以定义个 ApplicationModule
如下:
@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {
@Provides
fun provideMyApplication(application: Application): MyApplication {
return application as MyApplication
}
}
使用如下:
class Driver @Inject constructor(val application: MyApplication) {
}
3.9 ViewModel 的依赖注入
在 MVVM 架构中,ViewModel 层只是依赖于仓库层,它并不关心仓库的实例是从哪儿来的,因此由 Hilt 去管理仓库层的实例创建再合适不过了。
常见的 ViewModel 依赖注入过程如下:
首先有个仓库 Repository
类:
// 由于 Repository 要依赖注入到 ViewModel 当中,所以需要加上 @Inject 注解
class Repository @Inject constructor() {
...
}
然后有一个 MyViewModel
继承自 ViewModel
,用于表示 ViewModel
层:
// @HiltViewModel 注解,是专门为 ViewModel 提供的
// 构造函数中要声明 @Inject 注解,在 Activity 中才能使用依赖注入的方式获得 MyViewModel 的实例
@HiltViewModel
class MyViewModel @Inject constructor(val repository: Repository) : ViewModel() {
...
}
接下来修改 MainActivity
如下:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var viewModel: MyViewModel
...
}
这样在 MainActivity
中就可以通过依赖注入的方式得到 MyViewModel
的实例了。
当然如果有引入类似 Activity
扩展库 ktx:
// ktx
implementation "androidx.activity:activity-ktx:1.1.0"
那么上面 MainActivity
可修改如下:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// 此时无需声明 @Inject 注解
private val viewModel: MyViewModel by viewModels()
...
}
以上就是 ViewModel 的依赖注入。
本篇文章就介绍到这。
参考链接: