Hilt是什么,有什么用?
个人认为学习Hilt应该先学习Dagger2,毕竟hilt就是对dagger2的进一步封装
Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。
Hilt 通过为项目中的每个 Android 类提供容器并自动管理其生命周期,提供了一种在应用中使用 DI(依赖项注入)的标准方法。Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。
Hilt的官方文档
可以查看这个链接,这是dagger2的使用教程,学习Hilt您应该了解这些,不然很多名字注解看起来会比较生僻,除非您仅仅想使用它,并不想深究。
结合具体实例
本文旨在通过一个具体的实例来阐述Hilt的作用和用法, 根据官方教程的流程具体制造一个实例并进行更详细的分析。
引入Hilt
用android studio创建一个安卓基于Kotlin的工程 HiltBasic
修改build.gradle(project)
buildscript {
ext{
kotlin_version = "1.4.21"
hilt_version = "2.37"//hilt support
}
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
//hilt support begin
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
//hilt support end
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
接下来修改该build.gradle(app)
apply plugin: 'kotlin-kapt'//hilt support
apply plugin: 'dagger.hilt.android.plugin'//hilt support
...
android {
...
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
}
...
//hilt support begin
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
//hilt support end
Hilt 应用类
所有使用 Hilt 的应用都必须包含一个带有 @HiltAndroidApp 注释的 Application 类。
@HiltAndroidApp 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。
那么在我们的实例中创建这样一个Application类
@HiltAndroidApp
class MyApplication:Application() {
}
生成的这一 Hilt 组件会附加到 Application 对象的生命周期,并为其提供依赖项。此外,它也是应用的父组件,这意味着,其他组件可以访问它提供的依赖项。我们查看生成的源码可以看到一个Hilt_MyApplication类,这是此注解生成类之一,它是hilt组建全局的管理者。(别忘记修改AndroidManifest.xml)
将依赖项注入 Android 类
在 Application 类中设置了 Hilt 且有了应用级组件后,Hilt 可以为带有 @AndroidEntryPoint 注释的其他 Android 类提供依赖项,现在我们先在我们熟悉的MainActivity添加这个注释,看看会发生什么:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}
}
编译后我们发现多出了一个生成类"Hilt_MainActivity", 现在通过这个注解使得我们的MainActivity成了“Hilt_MainActivity”的子类,再次重申一下,如果您不想云里雾里,应该学习Dagger2,否则很多名字看起来很生僻。
如果您使用 @AndroidEntryPoint 为某个 Android 类添加注释,则还必须为依赖于该类的 Android 类添加注释。例如,如果您为某个 Fragment 添加注释,则还必须为使用该 Fragment 的所有 Activity 添加注释。
现在我们的MainActivity已经被注释过了,但这有什么好处呢? 咱们来分析一下(再此不在重复解释什么是依赖注入,以及它的好处了)
假设我们在MainActivity类中有一个User对象,在点击按钮后把User打印出来。
为了实现此目的, 在项目中添加一个文件di.kt, 并定义User(我已经假设您了解Dagger2咯)
package com.study.hiltbasic
import javax.inject.Inject
class User @Inject constructor()
{
var name:String=""
var age = 0
override fun toString()="User(name=$name age$age)"
}
修改MainActivity,添加一个按钮,点击按钮后设置并打印User
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var user:User//定义一个User对线
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonLogUser.setOnClickListener {
user.name = "朱Bony"
user.age = 30
textViewInfo.text = user.toString()
}
}
}
现在你可能会说:"菜鸟,你这代码会崩溃吧,User都没创建",我说:"我们先运行看看吧", 运行后发现并没有崩溃,一切正常:
这就是依赖注入,这就是Hilt最基本的使用, 它帮我们创建了User, 如果用Dagger2的话我们还得自己写Component,还得自己去创建Component,还得调用注入方法. 那么Hilt把这些活都帮我们做了。(更熟悉Dagger2的我表示,可能更明确的写出来会好点,但Hilt确实帮我们省去了不少步骤)。想了解为啥会这样,多看它生成的代码吧,我如果再此逐步分析的话就不叫使用教程了。
了解Dagger2的话,您应该已经感觉到了,这玩意就是为了帮我们节省时间的。但由于省去了很多步骤,导致代码有时候感觉挺突兀的,比如上面的User
当构造函数不能直接构造时
有时,类型不能通过构造函数注入。发生这种情况可能有多种原因。例如,您不能通过构造函数注入接口。此外,您也不能通过构造函数注入不归您所有的类型,如来自外部库的类。在这些情况下,您可以使用 Hilt 模块向 Hilt 提供绑定信息。
Hilt 模块是一个带有 @Module
注释的类。与 Dagger 模块一样,它会告知 Hilt 如何提供某些类型的实例。与 Dagger 模块不同的是,您必须使用 @InstallIn
为 Hilt 模块添加注释,以告知 Hilt 每个模块将用在或安装在哪个 Android 类中。
您在 Hilt 模块中提供的依赖项可以在生成的所有与 Hilt 模块安装到的 Android 类关联的组件中使用。
还是按照官方教程的顺序来吧
使用 @Binds 注入接口实例
为了说明@Binds注解,我们在我们的di.kt中添加一个接口两个类,来看代码
interface Engine{
fun on()
fun off()
}
class ChinaEngine @Inject constructor():Engine{
override fun on() {
Log.i("zrm", "ChinaEngine on")
}
override fun off() {
Log.i("zrm", "ChinaEngine off")
}
}
class ChinaCar @Inject constructor(val engine:Engine){
lateinit var name:String
}
我们定义了一个接口Engine,还有ChinaEngine 和ChinaCar类, 我们再在MainActivity中定义一个ChinaCar实例
@Inject lateinit var chinaCar:ChinaCar
现在我们发现这个chinaCar的构造函数需要一个接口,所以不能通过构造函数注入,那么我们怎么告诉Hilt我们的chinacar需要一个实际上为ChinaEngine的实例呢(注意ChinaEngine是可以用构造函数注入的), @Binds就是干这事儿的。(实际上我在Dagger2教程已经讲过了)
在我们的di.kt添加如下代码:(因为是实例,所有代码都写在了一个文件,实际上应该分开)
@Module
@InstallIn(ActivityComponent::class)//告诉Hilt 这个module属于的Component,ActivityComponent是Hilt定义好的
interface MainModule {
@Binds
fun bindEngine(chinaEngine:ChinaEngine):Engine
}
现在Hilt就知道怎么创建ChinaCar了,试试吧:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var user:User//定义一个User对线
@Inject lateinit var chinaCar:ChinaCar
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
buttonLogUser.setOnClickListener {
user.name = "朱Bony"
user.age = 30
chinaCar.name="比亚迪"
textViewInfo.text = user.toString()
chinaCar.engine.on()
chinaCar.engine.off()
}
}
}
一切正常,log正常打印出来了。
下面再看一种不能用构造函数注入的情况
接口不是无法通过构造函数注入类型的唯一一种情况。如果某个类不归您所有(因为它来自外部库,如 Retrofit、OkHttpClient
或 Room 数据库等类),或者必须使用构建器模式创建实例,也无法通过构造函数注入。
接着前面的例子来讲。如果 Dog类需要一个构造参数,Hilt 如何提供此类型的实例? 方法是在 Hilt 模块内创建一个函数,并使用 @Provides 为该函数添加注释。
带有注释的函数会向 Hilt 提供以下信息:
- 函数返回类型会告知 Hilt 函数提供哪个类型的实例。
- 函数参数会告知 Hilt 相应类型的依赖项。
- 函数主体会告知 Hilt 如何提供相应类型的实例。每当需要提供该类型的实例时,Hilt 都会执行函数主体。
先来写Dog类:
data class Dog(val name:String)
添加新的Module用来提供dog的创建办法
@Module
@InstallIn(ActivityComponent::class)
class DogModule
{
@Provides
fun provideDog()=Dog("京巴犬")
}
现在可以定义一个dog来试试,一切okay
@Inject lateinit var dog:Dog
现在有人说了 我有两只狗一个叫“京巴”一个叫“泰迪”, 还有一辆用美国引擎的国产车, 你的需求很正常, 请继续看
为同一类型提供多个绑定
如果您需要让 Hilt 以依赖项的形式提供同一类型的不同实现,必须向 Hilt 提供多个绑定。您可以使用限定符为同一类型定义多个绑定。
限定符是一种注释,当为某个类型定义了多个绑定时,您可以使用它来标识该类型的特定绑定。
仍然接着前面的例子来讲。首先,定义要用于为 @Binds
或 @Provides
方法添加注释的限定符, 关于限定符更详细的介绍可以看我的Dagger2教程,下面直接上代码了,先来定义两个限定符:
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class MadeInCN
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class MadeInUSA
除了自定义限定符,还可以用Hilt(dagger2)已经定义好的@Named,这个后边会解释。 中国车也可以有美国引擎,我们先来添加一个美国引擎
class AmericaEngine @Inject constructor():Engine{
override fun on() {
Log.i("zrm", "AmericaEngine on")
}
override fun off() {
Log.i("zrm", "AmericaEngine off")
}
}
通过限定符来区分中国引擎和美国引擎,并告诉Hilt
@Module
@InstallIn(ActivityComponent::class)
class MainModule {
@Provides
@MadeInCN
fun provideChinaCar():ChinaCar
{
return ChinaCar(ChinaEngine())
}
@Provides
@MadeInUSA
fun provideChinaCar2():ChinaCar
{
return ChinaCar(AmericaEngine())
}
}
在主界面定义两台车,分别用不通的引擎
@Inject @MadeInCN lateinit var chinaCar:ChinaCar
@Inject @MadeInUSA lateinit var chinaCar2:ChinaCar
测试一下,一切正常
Hilt 中的预定义限定符
Hilt 提供了一些预定义的限定符。例如,由于您可能需要来自应用或 Activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。
假设本例中的 User类需要 Activity 的上下文。以下代码演示了如何向 User提供 Activity 上下文
class User @Inject constructor(@ActivityContext val context: Context) {
var name: String = ""
var age = 0
override fun toString() = "User(name=$name age$age)"
fun showMsg() = Toast.makeText(context, toString(), Toast.LENGTH_SHORT).show()
}
在主界面调用这个showMsg, toast正常弹了出来。 这说明context参数Hilt帮我们获取并保持了,当然了从结构化设计以及生命周期等等方面来说,不应该在user类中有context这种东西,本例旨在说明知识点,请勿较真儿。
如需了解 Hilt 中提供的其他预定义绑定,请参阅组件默认绑定。
为 Android 类生成的组件
对于您可以从中执行字段注入的每个 Android 类,都有一个关联的 Hilt 组件,您可以在 @InstallIn 注释中引用该组件。每个 Hilt 组件负责将其绑定注入相应的 Android 类。
前面的示例演示了如何在 Hilt 模块中使用 ActivityComponent。
组件生命周期
Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。
组件作用域
默认情况下,Hilt 中的所有绑定都未限定作用域。这意味着,每当应用请求绑定时,Hilt 都会创建所需类型的一个新实例。
在本例中,每当 定义一个User都将创建一个新实例。
不过,Hilt 也允许将绑定的作用域限定为特定组件。Hilt 只为绑定作用域限定到的组件的每个实例创建一次限定作用域的绑定,对该绑定的所有请求共享同一实例。
下表列出了生成的每个组件的作用域注释
在本例中,如果您使用 @ActivityScoped 将 User的作用域限定为 ActivityComponent,Hilt 会在相应 Activity 的整个生命周期内提供 User的同一实例:
@ActivityScoped
class User @Inject constructor(@ActivityContext val context: Context) {
var name: String = ""
var age = 0
override fun toString() = "User(name=$name age$age)"
fun showMsg() = Toast.makeText(context, toString(), Toast.LENGTH_SHORT).show()
}
为了测试在Activity内单例,我们在定义一个User
@Inject lateinit var user:User//定义一个User对线
@Inject lateinit var user2:User
我们通过调试,打印等发现两个User确实是同一个了,如果有兴趣,可以在创建一个新的Activity看看两个activity下的的User是否相同,以此来验证作用域
注意:将绑定的作用域限定为某个组件的成本可能很高,因为提供的对象在该组件被销毁之前一直保留在内存中。请在应用中尽量少用限定作用域的绑定。如果绑定的内部状态要求在某一作用域内使用同一实例,或者绑定的创建成本很高,那么将绑定的作用域限定为某个组件是一种恰当的做法。
其它的作用域也是同理,可以自己试验下,值得一提的是作用域限定为 SingletonComponent(@Singleton),这相当于在整个App内单例,这个可能是我们很多单例时会使用的情况。
注意:默认情况下,如果您在视图中执行字段注入,ViewComponent 可以使用 ActivityComponent 中定义的绑定。如果您还需要使用 FragmentComponent 中定义的绑定并且视图是 Fragment 的一部分,应将 @WithFragmentBindings 注释和 @AndroidEntryPoint 一起使用。
组件默认绑定
每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入您自己的自定义绑定。请注意,这些绑定对应于常规 Activity 和 Fragment 类型,而不对应于任何特定子类。这是因为,Hilt 会使用单个 Activity 组件定义来注入所有 Activity。每个 Activity 都有此组件的不同实例。
这段话的意思是, 每个安卓组件(activity, fragment, application。。。)都对应某个Hilt组件的一个实例,对应表见上图。
在 Hilt 不支持的类中注入依赖项
个人认为不支持的可以直接使用Dagger就行了,但还是看看Hilt的正规做法
Hilt 支持最常见的 Android 类。不过,您可能需要在 Hilt 不支持的类中执行字段注入。
在这些情况下,您可以使用 @EntryPoint 注释创建入口点。入口点是由 Hilt 管理的代码与并非由 Hilt 管理的代码之间的边界。它是代码首次进入 Hilt 所管理对象的图的位置。入口点允许 Hilt 使用它并不管理的代码提供依赖关系图中的依赖项。
@EntryPoint用于将接口标记为生成组件的入口点。这个注释必须与InstallIn一起使用,以指示哪个组件应该拥有这个入口点。在装配组件时,Dagger将使所指示的组件扩展此标注的接口。
这东西就是假设您在安卓不支持的组件或者什么东西中,想注入字段什么的就可以使用它。我们还是来结合实例说明一下,不然很难理解。(可能Hilt和dagger混用不好,但个人认为用dagger2就好了。。。)。
为了说明情况,现在我们改造dog类 让它拥有一个注入的wokr字段,那怎么让Hilt提供这个work的注入(创建)呢?
class Work @Inject constructor()
{
lateinit var workName:String
}
data class Dog @Inject constructor(val name:String)
{
@Inject lateinit var work: Work
}
...
dog.work.workName="导盲"
此时我们运行程序,发现执行赋值“导盲”时崩溃了,因为Hilt不知道如何创造Dog下的work,没有任何组件告诉Hilt怎么做这件事。我先用dagger2来完成这个事情,然后再说Hilt。这样也能有个对比,我已经假设您比较熟悉dagger2了,学习dagger2 看其他文章吧
首先添加一个Component在di.kt
@Component
interface DogComponent{
fun inject(dog:Dog)
}
修改dog来调用注入work
data class Dog @Inject constructor(val name:String)
{
@Inject lateinit var work: Work
init {
DaggerDogComponent.create().inject(this)
}
}
好,现在再次运行程序,发现一切正常了,那么同样的效果,用Hilt怎么实现呢?首先把原来的Dagger代码注释掉
然后改造Dog类
class Dog @Inject constructor(@ApplicationContext appContext:Context, val name:String)
{
@Inject lateinit var work: Work
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface DogEntries
{
fun provideWork():Work
}
init {
work = EntryPoints.get(appContext, DogEntries::class.java).provideWork()
}
}
此时我们发现程序可以正常运行了,看起来比dagger确实简单点? 但多需要一个context参数,context这种东西,在安卓中本来就是无处不在。。。注意原来提供狗狗的模块要更新下哦
@Module
@InstallIn(ActivityComponent::class)
class DogModule
{
@Provides
@Named("jingba")
fun provideDog1(@ApplicationContext appContext:Context)=Dog(appContext,"京巴犬")
@Provides
@Named("taidi")
fun provideDog2(@ApplicationContext appContext:Context)=Dog(appContext,"泰迪")
}
个人感觉讲到这里,剩下的就是自己去实践,去继续熟练并采坑了。
结语
dagger2和hilt到底该用谁,在安卓中还是用Hilt吧,这肯定是趋势,不然Google的官方文档就不会说它了。 但就目前来说,实际项目中可能hilt用的还是比较少。
最后把代码全部复制一下
di.kt
package com.study.hiltbasic
import android.content.Context
import android.util.Log
import android.widget.Toast
import dagger.*
import dagger.hilt.EntryPoint
import dagger.hilt.EntryPoints
import dagger.hilt.InstallIn
import dagger.hilt.android.components.ActivityComponent
import dagger.hilt.android.components.ApplicationComponent
import dagger.hilt.android.qualifiers.ActivityContext
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.android.scopes.ActivityScoped
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Qualifier
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MadeInCN
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MadeInUSA
class Work @Inject constructor()
{
lateinit var workName:String
}
@ActivityScoped
class User @Inject constructor(@ActivityContext val context: Context) {
var name: String = ""
var age = 0
override fun toString() = "User(name=$name age$age)"
fun showMsg() = Toast.makeText(context, toString(), Toast.LENGTH_SHORT).show()
}
interface Engine{
fun on()
fun off()
}
class ChinaEngine @Inject constructor():Engine{
override fun on() {
Log.i("zrm", "ChinaEngine on")
}
override fun off() {
Log.i("zrm", "ChinaEngine off")
}
}
class AmericaEngine @Inject constructor():Engine{
override fun on() {
Log.i("zrm", "AmericaEngine on")
}
override fun off() {
Log.i("zrm", "AmericaEngine off")
}
}
class ChinaCar @Inject constructor(val engine:Engine){
lateinit var name:String
}
class Dog @Inject constructor(@ApplicationContext appContext:Context, val name:String)
{
@Inject lateinit var work: Work
@EntryPoint
@InstallIn(ApplicationComponent::class)
interface DogEntries
{
fun provideWork():Work
}
init {
work = EntryPoints.get(appContext, DogEntries::class.java).provideWork()
}
}
@Module
@InstallIn(ActivityComponent::class)
class MainModule {
@Provides
@MadeInCN
fun provideChinaCar():ChinaCar
{
return ChinaCar(ChinaEngine())
}
@Provides
@MadeInUSA
fun provideChinaCar2():ChinaCar
{
return ChinaCar(AmericaEngine())
}
}
@Module
@InstallIn(ActivityComponent::class)
class DogModule
{
@Provides
@Named("jingba")
fun provideDog1(@ApplicationContext appContext:Context)=Dog(appContext,"京巴犬")
@Provides
@Named("taidi")
fun provideDog2(@ApplicationContext appContext:Context)=Dog(appContext,"泰迪")
}
//@Component
//interface DogComponent{
// fun inject(dog:Dog)
//}
//@Component
//interface DogComponent
//{
// fun makeDog():Dog
// @Component.Builder
// interface Builder{
// @BindsInstance
// fun name(name:String):Builder
// fun build():DogComponent
// }
//}
MainActivity.kt
package com.study.hiltbasic
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import dagger.hilt.EntryPoint
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_main.*
import javax.inject.Inject
import javax.inject.Named
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var user:User//定义一个User对线
@Inject lateinit var user2:User
@Inject @MadeInCN lateinit var chinaCar:ChinaCar
@Inject @MadeInUSA lateinit var chinaCar2:ChinaCar
@Inject @Named("jingba") lateinit var dog:Dog
@Inject @Named("taidi") lateinit var dog2:Dog
lateinit var dog3:Dog
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
//dog3 = DaggerDogComponent.builder().name("二哈").build().makeDog()
buttonNew.setOnClickListener {
startActivity(Intent(this, ChildActivity::class.java))
}
buttonLogUser.setOnClickListener {
user.name = "朱Bony"
user.age = 30
chinaCar.name="比亚迪"
dog.work.workName="导盲"
textViewInfo.text = user.toString() +
"\r\n" +
dog.toString() +
"\r\n" +
dog2.toString()// +
//"\r\n" +
//dog3.toString()
chinaCar.engine.on()
chinaCar.engine.off()
chinaCar2.engine.on()
chinaCar2.engine.off()
user.showMsg()
user2.showMsg()
}
}
}
好了,似乎篇幅过长了.