Jetpack - Hilt

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 类中创建了两台电脑的实例,然后再对它们进行配送。卡车既要会送货,也要会生产电脑,使得卡车和电脑这两样不相干的东西耦合到一起去了,造成耦合度过高。

若又接到一个新的订单,去配送手机,那这辆卡车还要会生产手机才行。若增加配送蔬果的订单,那么这辆卡车还要会种地。。。最后发现,这已经就不是一辆卡车了,而是一个商品制造中心了:

卡车.png

其实,卡车并不需要关心配送的货物具体是什么,它的任务只需负责送货。即卡车是依赖于货物的,给了卡车货物,它就去送货,不给卡车货物,它就待命,修改代码如下:

// 在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() 函数中,为了让卡车送货,自己制造了一辆卡车。这明显是不合理的,电脑公司应该只负责生产电脑,它不应该去生产卡车。

更加合理的做法是,让卡车配送公司派辆空闲的卡车过来(就不用自己造车了),当卡车到达后,再将电脑装上卡车,然后执行配送任务即可。如下:

卡车-电脑-配送公司.png

使用这种设计结构,就有很好的扩展性。若现在又有一家蔬果公司需要找一辆卡车来送菜,就完全可以使用同样的结构来完成任务,如下:

卡车-电脑-配送公司-其他.png

上图中呼叫卡车公司并让他们安排空闲车辆的这个部分,其实可以通过自己手写来实现,也可借助一些依赖注入框架来简化这个过程。

因此,依赖注入框架的作用就是为了替换下图所示的部分:

依赖注入框架的作用.png

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 注解,如给 OkHttpClientRetrofit 类型提供实例如下:

@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 内置组件.png

Hilt 一共提供了7种组件作用域注解,和上面的7个内置组件分别是一一对应的,如下表:


Hilt 组件作用域.png

若想要在全程序范围内共用某个对象的实例,那么就使用 @Singleton

若想要在某个 Activity,以及它内部包含的 FragmentView 中共用某个对象的实例,那么就使用@ActivityScoped

以此类推。。。

作用域的包含关系如下:


作用域的包含关系.png

即,对某个类声明了某种作用域注解之后,这个注解的箭头所能指到的地方,都可以对该类进行依赖注入,同时在该范围内共享同一个实例。

@Singleton 注解的箭头可以指向所有地方。
@ServiceScoped 注解的箭头无处可指,所以只能限定在 Service 自身当中使用。
@ActivityScoped 注解的箭头可以指向FragmentView 当中。

3.8 Hilt 中的预定义限定符

Hilt 提供了一些预定义的限定符。

例如,需要来自应用或 ActivityContext 类,就可以用 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 的依赖注入。

本篇文章就介绍到这。


参考链接:

Jetpack新成员,一篇文章带你玩转Hilt和依赖注入

使用 Hilt 实现依赖项注入(官网)

从 Dagger 到 Hilt,谷歌为什么执着于让我们使用依赖注入?

推荐阅读更多精彩内容