Android进阶宝典 -- Hilt的使用

在上一节中,我们简单介绍了Dagger2的使用,其实我们在使用Dagger2的时候,发现还是比较繁琐的,要自己写Module、Component、Provides等等,于是Hilt的团队就和Dagger2的团队一起,设计了面向Android移动端的依赖注入框架 -- Hilt

1 Hilt配置

在项目级的build.gradle中,引入支持hilt的插件,注意官方文档中的2.28-alpha版本可能有文件,建议使用下面的版本

classpath "com.google.dagger:hilt-android-gradle-plugin:2.43.2"

app的build.gradle中引入插件

id 'dagger.hilt.android.plugin'

引入依赖

implementation "com.google.dagger:hilt-android:2.43.2"
kapt "com.google.dagger:hilt-android-compiler:2.43.2"

2 Hilt的使用

首先按照惯例,先写一个Module

@Module
class RecordModule {
    @Provides
    fun providerRecord():Record{
        return Record()
    }
}

如果是Dagger2的写法,需要再写一个Component,将RecordModule加载进去,那么Hilt就不要这一步,而是需要一个注解InstallIn来声明这个Module使用在哪个地方

@InstallIn(ApplicationComponent::class)
@Module
class RecordModule {
    @Provides
    fun providerRecord(): Record {
        return Record()
    }
}

在Hilt中有以下几个Component,我这里拿几个典型说一下

image.png

首先ApplicationComponent,它会存在整个App生命周期中,随着App的销毁而销毁,也就意味着,在App的任何位置都可以使用这个Module

//A Hilt component that has the lifetime of the application
@Singleton
@DefineComponent
public interface ApplicationComponent {}

像ActivityComponent,肯定就是存在于整个Activity生命周期中,随着Activity的销毁而销毁

//A Hilt component that has the lifetime of the activity.
@ActivityScoped
@DefineComponent(parent = ActivityRetainedComponent.class)
public interface ActivityComponent {}

其他的都类似,都是随着组件的生命周期结束而消逝。

例如我们需要在MainActivity中注入某个类,那么需要使用@AndroidEntryPoint修饰,代表当前依赖注入的切入点

@AndroidEntryPoint
class MainActivity : AppCompatActivity()

同时,需要将当前app定义为Hilt App

@HiltAndroidApp
class MyApp : Application() {

    override fun onCreate() {
        super.onCreate()
    }
}

而且,在MainActivity中注入这个对象之后,也不需要像Dagger那样,去创建具体的Component对象

@Inject
@JvmField
var record:Record? = null

这样一看,Hilt是不是要比Dagger要简单许多了。

2.1 局部单例

@InstallIn(ActivityComponent::class)
@Module
class RecordModule {
    @Provides
    @ActivityScoped
    fun providerRecord(): Record {
        return Record()
    }
}

如果我们希望Record是一个单例对象,可以使用@ActivityScoped注解修饰,我们先看下效果

@Inject
@JvmField
var record: Record? = null

@Inject
@JvmField
var record2: Record? = null

在MainActivity中声明了两个对象,我们发现两个对象的hashcode是一致的,说明在当前Activity中这个对象就是单例,但是跳转到下一个Activity的时候,发现拿到的对象就是一个新的对象

2022-09-11 20:52:19.583 1860-1860/com.lay.image_process E/TAG: record 83544912record2 83544912
2022-09-11 20:53:11.071 1860-1860/com.lay.image_process E/TAG: record 163680212

也就是说,@ActivityScoped修饰的对象只是局部单例,并不是全局的;那么如何才能拿到一个全局的单例呢?其实在之前的版本中,有一个ApplicationComponent,其对应的作用域@Singleton拿到的对象就是全局单例,后来Google给移除了,我觉得Google之所以移除,可能就是推动大家采用数据共享设计模式。

Component 作用域(局部单例)
ActivityComponent ActivityScoped
FragmentComponent FragmentScoped
ServiceComponent ServiceScoped
ViewComponent ViewScoped
ViewModelComponent ViewModelScoped

上面是整理的使用比较频繁的Component,对应的局部单例作用域

2.2 为接口注入实现类

首先创建一个接口

interface MyCallback {
    fun execute()
}

然后创建一个实现类,这里需要注意,构造方法中如果需要传入上下文,那么需要使用@ApplicationContext修饰,其他参数则不需要

class MyCallBackImpl : MyCallback {

    private var context: Context? = null

    @Inject
    constructor(@ApplicationContext context: Context) {
        this.context = context
    }

    override fun execute() {
        Toast.makeText(context, "实现了", Toast.LENGTH_SHORT).show()
    }

    fun clear() {
        if (context != null) {
            context = null
        }
    }
}

那么在创建module的时候,方法需要定义为抽象方法,而且需要使用@Binds来获取实现类,方法同样是抽象方法,参数为具体实现类,返回值为接口。

@Module
@InstallIn(ActivityComponent::class)
abstract class ImplModule {
    @Binds
    abstract fun getImpl(impl: MyCallBackImpl): MyCallback
}

那么在使用时,就非常简单了,这里获取到的就是MyCallBackImpl实现类

@Inject
@JvmField
var callback: MyCallback? = null

那么大家想一个问题,如果我有多个实现类,那么如何区分这个MyCallback到底是哪个实现类呢?同样可以使用注解来区分

class MyCallbackImpl2 : MyCallback {

    private var context: Context? = null

    @Inject
    constructor(@ApplicationContext context: Context) {
        this.context = context
    }

    override fun execute() {
        Toast.makeText(context, "实现2", Toast.LENGTH_SHORT).show()
    }
}

这样的话,就有两个抽象方法,分别返回MyCallbackImpl2和MyCallBackImpl两个实现类,那么在区分的时候,就可以通过@BindImpl和@BindImpl2两个注解区分

@Module
@InstallIn(ActivityComponent::class)
abstract class ImplModule {
    @Binds
    @BindImpl
    abstract fun getImpl(impl: MyCallBackImpl): MyCallback

    @Binds
    @BindImpl2
    abstract fun getImpl2(impl2: MyCallbackImpl2): MyCallback
}

在调用时,同样需要使用@BindImpl2或者@BindImpl来区分获取哪个实现类

@Inject
@JvmField
@BindImpl2
var callback: MyCallback? = null

其实@BindImpl注解很简单,就是通过@Qualifier注解来区分

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class BindImpl2 {
}

3 Hilt原理

大家看到在调用这个注入对象的时候,发现没有看到对应的入口,并没有DaggerComponent那么显眼,其实编译时技术很多都是这样,是要从打包编译文件夹去找


image.png

我们可以看到,hilt是自己单独的一个文件夹,其中就有生成的资源文件

private MyCallbackImpl2 myCallbackImpl2() {
  return new MyCallbackImpl2(ApplicationContextModule_ProvideContextFactory.provideContext(singletonCImpl.applicationContextModule));
}

private MainActivity injectMainActivity3(MainActivity instance) {
  MainActivity_MembersInjector.injectRecord(instance, providerRecordProvider.get());
  MainActivity_MembersInjector.injectRecord2(instance, providerRecordProvider.get());
  MainActivity_MembersInjector.injectCallback(instance, myCallbackImpl2());
  return instance;
}

其实我们可以看到,这种实现方式跟Dagger2其实是一样的,同样都是在内部初始化了某个类,例如MyCallbackImpl2,其context是由ApplicationContextModule提供的

@InjectedFieldSignature("com.lay.image_process.MainActivity.callback")
@BindImpl2
public static void injectCallback(MainActivity instance, MyCallback callback) {
  instance.callback = callback;
}

调用injectCallback就是将MainActivity中的callback赋值,获取的就是MyCallbackImpl2实现类。

其实Hilt的内部实现原理跟Dagger2是一样,只是做了进一步的封装,所以如果理解了之前Dagger2的原理,相比Hilt也不在话下了。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容