Android进阶宝典 -- Hilt的使用

语言: CN / TW / HK

在上一节中,我们简单介绍了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的时候,发现拿到的对象就是一个新的对象

java 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两个注解区分kotlin @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来区分获取哪个实现类kotlin @Inject @JvmField @BindImpl2 var callback: MyCallback? = null 其实@BindImpl注解很简单,就是通过@Qualifier注解来区分kotlin @Qualifier @Retention(AnnotationRetention.RUNTIME) annotation class BindImpl2 { } ```

3 Hilt原理

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

image.png

我们可以看到,hilt是自己单独的一个文件夹,其中就有生成的资源文件 java private MyCallbackImpl2 myCallbackImpl2() { return new MyCallbackImpl2(ApplicationContextModule_ProvideContextFactory.provideContext(singletonCImpl.applicationContextModule)); } java 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也不在话下了。