Jetpack学习之 Hilt

网友投稿 996 2022-11-17

Jetpack学习之 Hilt

Jetpack学习之 Hilt

目录

​​1. 概述​​

​​1.1 依赖注入(DI)概念​​​​1.2 为什么在代码中使用外部去注入依赖​​

​​1.2.1 满足合理的架构设计​​​​1.2.2 满足一些数据共享场景​​

​​1.3 Hilt是什么​​​​1.4 Hilt使用地方​​

​​2. Hilt使用​​

​​2.1 导入​​​​2.2 一个简单的例子​​​​2.3 实现接口实例注入​​​​2.4 实现第三方依赖注入​​​​2.5 Hilt 的内置组件和作用域​​

​​2.5.1 @InstallIn 注解​​​​2.5.2 使注入对象单例​​​​2.5.3 作用域的包含关系​​

​​2.6 Hilt 预置的 Qualifier​​

​​3 小结​​​​参考文章​​

Git学习地址:​​传送门​​

1. 概述

随着Android 11 的发布,Jetpack家族新添了 ​​Hilt​​​ 和 ​​App Startup​​等成员。

而 Hilt 是被定义为 依赖注入框架而被发布。什么?又是依赖注入框架?不是之前已经有了一个 ​​Dagger2​​​ 了吗?除了 Dagger2, 还有 ​​ButterKnife​​​ ,Kotlin甚至还有轻量级的 ​​Koin​​。

为什么?为什么谷歌这么 这么的想让我们去了解依赖注入,并使用依赖注入的框架呢?下面请听我慢慢分析。

1.1 依赖注入(DI)概念

什么是依赖注入?先来看下面代码:

class MyClass { val user = User()}

上面这段代码就产生了一个依赖关系。

我们要先看懂谁依赖了谁?首先 ​​MyClass​​​ 是我们的类,​​User​​​ 可以是我们自己写的类,也可以是通过第三方Jar包或SDK里面的类。 在我们写的 ​​​MyClass​​ 的代码里面,我们需要一个 User 对象来完成一些任务,所以我们创建了 User 对象,这就说明 MyClass 依赖了 User。对于 MyClass来说,User是外面之物,但是又需要依赖它。

如果上面这个 User,不是由自己创建,而是由外部创建,然后在本类只做赋值工作 ,这个过程就是 依赖注入。

有一个我们非常熟悉的设计模式,就使用了依赖注入的方法—工厂模式:

class UserFactory { fun newUser(): User{ return User() }}class MyClass { val user = UserFactory.newUser()}

我们的 MyClass 类需要使用 User 类,但是这次没有自己来创建(没有自己new出来),而是交由给 ​​UserFactory​​ 来创建出来,MyClass就做了最后的赋值工作。

对于 MyClass 来说,这就是一次依赖注入,和上面例子相比,把对象创建的过程交由给了别的类。

所以我们通过上面两个例子就能知道依赖注入的本质是什么:借由外部得到对象。依赖注入框架就是这个外部

现在流行的 Dagger2、Koin框架,只是让我们更轻松、更容易的去得到对象。

Dagger的中文翻译是 “刀”,它就像一把刀,插进我们代码中。那相信你也知道 ​​ButterKnife​​ 为什么这么取名了吧。

PS: 对于我来说,针筒注射更适合来描述这个过程。

1.2 为什么在代码中使用外部去注入依赖

1.2.1 满足合理的架构设计

举一个Andorid常用的例子。 在 ​​MVP / MVC / MVVM​​​ 架构没有流行之前,我们的代码都是如何的? 我们会把所有的 数据逻辑代码、视图代码都写在了 Activity / Fragment 中。

这会导致什么结果:

Activity / Fragment 代码臃肿,逻辑混乱难维护,难复用

举一个代码例子,我们要在冰箱中放一个苹果,那么我们代码会这样写:

class Fridge { val apple = Apple() // 创建一个苹果 fun store() { storeAnApple(apple) // 存放苹果 }}

这样写有没有问题?如果你觉得没有问题,那我的文章你应该继续往下读,如果觉得有问题,那你可以跳过这整节了。

​​Fridge​​​ 是冰箱类,那么按照 ​​单一职责原则​​​,它的作用应该是存储物品。但是在代码中,它却做了一个事情:那就是将苹果创建出来了。冰箱类,它不应该知道苹果是如何产生的,它只需要拿到苹果并存储就行了。不然的话,冰箱要存储香蕉,那岂不是又new出一个香蕉,要放水果蔬菜,又要new出水果蔬菜…

显然,我们不需要 Fridge 来创建 Apple 对象,所以我们需要外部来帮我创建。比如苹果工厂。

或者我们使用 依赖注入框架 Dagger2,通过类似下面的代码:

class Fridge { @Inject lateinit val apple: Apple // 从DI框架中拿到一个苹果 fun store() { storeAnApple(apple) // 存放苹果 }}

这样,冰箱就可以不用关心苹果的构造,而直接存储了。

1.2.2 满足一些数据共享场景

假设我们 ​​Activity​​ 有一个自定的TextView,并且该 TextView 需要依赖 MainActivity 的数据,那么这个TextView要这样写:

@AndroidEntryPointclass MainActivity : AppCompatActivity() { @Inject lateinit var user: User ...}class MyTextView: TextView(...) { ... // 需要用到 MainActivity中的 user 对象 override fun onAttachedToWindow() { super.onAttachedToWindow() // 强制转化 text = (context as MainActivity).user.name }}

这样的代码很丑,因为做了强转,并且只适用于 ​​MainActivity​​,不灵活。

而如果使用了Dagger、Hilt框架,就能这样写:

@ActivityScopeddata class User(var id: Int, var name: String, var mood: String) { ...}@AndroidEntryPointclass MyTextView(context: Context?, attrs: AttributeSet?) : TextView(context, attrs) { @Inject lateinit var user: User override fun onAttachedToWindow() { super.onAttachedToWindow() // 直接使用 MainActivity的 text = user.name }}

这里就能直接使用到 MainActivity的user对象。相当于 MainActivity 中注入后 User 后,又在代码的运行中将该数据分享到了 MyTextView 中。

在我们的代码中,就有许多需要数据共享的地方,比如 ​​OkHttp / Retrofit​​ 的单例,一些数据Bean等。

那这里有一个问题:假如我的数据不需要被共享,只在一处用,那我还要给它做依赖注入吗? 答案是:根据设计原则的扩展性,我们不能在一开始就断定一个类之后的迭代中是否会被各种类使用、继承,既然我们保证不了其以后不被共享,那我们就可以在一开始设计时,给它使用依赖注入的形式被创建,这样便于以后的迭代。

1.3 Hilt是什么

了解了 依赖注入 是什么之后,我们再来了解 Hilt,反正我们知道他是一种得到对象的手段。

Hilt 是基于 Dagger2 的针对 Android场景定制化 的框架。

这有点像什么? RxAndroid 是 RxJava 的Android平台定制化扩展。Andorid虽然由Java、Kotlin构成,但是它有很多平台的特性,比如它有 Java开发 所不知道的 ​​Context​​ 等。

Dagger框架虽然很出名,在国外也很流行,但是在国内使用其的App少之又少,列举一些缺点:

上手难,众多Android应用框架中,Dagger必定是最难学的那一档;它就是一个复杂框架,没有针对任何平台,所以对于所有平台来说会难用;在Android Studio4.0版本以前,无法追踪Dagger的依赖关系(就类比IDE无法通过快捷键知道一个接口有哪些实现类)开发者不知道为啥要做依赖注入

对于第三点,Android Studio4.1已经支持了该功能,但是4.1有许多Bug,很多开发者都没有升级 = =。

至于 ​​Koin​​​ 和 ​​Hilt​​​ 的对比,两者配置的代码都比较少, 所以比较的层次更多在性能方面,我因为没有学习过Koin,所以这里就不做比较,可以看看这篇文章: ​​全方面分析 Hilt 和 Koin 性能​​

1.4 Hilt使用地方

​​Daager​​​、​​Koin​​​、​​Hilt​​​,Google为什么致力于让开发者使用DI框架,可以看下Android开发者文档的这篇文章:​​传送门​​

Google认为移动端应用的架构设计,最重要的 Separation of concerns(分离关注点)。上网找解释,其实它就是 模块解耦。下面是Google官方推荐的Android应用架构图:

该图不多做说明,郭神的文章中就已经分析了该图片,Jetpack完全就是为该图架构服务。

Activity / Fragment是由系统所创建的,所以我们可以不用关心如何去创建。 ViewModel 层由 ​​JetPack​​​ 来提供,所以我们也不用关心其创建。但是该有谁来创建 ​​Repository​​​呢? 该架构下,Activity是不知道 Repository 存在的, 而 ViewModel 显然不能来创建 Repository,因为它只是依赖。 如果我们将Repostirory 设置成单例类,那每处地方都能够引用它,这样的处理显得有些不好。

这个问题比较棘手,但是如果我们使用 依赖注入框架,就能灵活的解决这个问题了。

2. Hilt使用

2.1 导入

在 app 的 ​​build.gradle​​ 中加入:

plugins { ... id 'kotlin-kapt' id 'dagger.hilt.android.plugin'}dependencies { ... implementation 'com.google.dagger:hilt-android:2.28-alpha' kapt 'com.google.dagger:hilt-android-compiler:2.28-alpha'}

在 project的 ​​build.gradle​​ 中加入:

classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'

2.2 一个简单的例子

Hilt 需要 ​​AndroidManifest​​​ 使用带有 ​​@HiltAndroidApp​​ 注解的 Application 类,所以我们的 Application需要这样:

@HiltAndroidAppclass HiltApp : Application() { ...}

为了简化这个问题,我定义一个默认的无参构造函数,反正创建之后,里面的值也是可以修改的嘛。

data class User(var name: String, var age: Int) { // 定义一个默认的无参构造函数,并使用 @Inject 注解修饰 @Inject constructor() : this("Rikka", 23)}

// 1@AndroidEntryPointclass MainActivity : AppCompatActivity() { // 2 @Inject lateinit var user: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) Log.d(TAG, "user name:${user.name} age:${user.age}") }}

Logcat 打印结果如下:

代码解析

注释1: 为 MainActivity 修饰 ​​@AndroidEntryPoint​​,该注解表明 该类为需要进行依赖注入的 Android类,是Dagger针对Android场景化的地方。当我们类中需要进行依赖注入,我们为该类加入这个注解,它会帮助创建一个单独的 Hilt组件。它不能修饰Abstract,它只能修饰:

ComponentActivity(Support)FragmentViewServiceBroadcastReceiver

Inject 的中文翻译是 “注入、注射”,所以可以形象的认为, ​​@Inject​​ 修饰的变量是被外界通过针筒注入进来的。

​​@Inject​​ 可以修饰

构造函数 Constructors变量 Fields方法 Methods

构造函数是最先被注解的,然后再是变量和方法。所以它修饰构造函数和修饰变量,其实是不同的作用。但为了便于理解,我们可以把它看成是一个插眼工具,便于Hilt去寻找要注入的地方。

我们上面的 User 类是无参构造函数,这次假设我们要有参数的呢?其实就是参数也要注入嘛,这就是套娃来的,来看看我们给User新增一个 属性:​​Clothes​​:

class Clothes @Inject constructor() {}class User @Inject constructor(var clothes: Clothes){}@AndroidEntryPointclass MainActivity : AppCompatActivity() { ... Log.d(TAG, "user clothes:${user.clothes}") }

打印结果:

PS:大家不要太拘泥于有参构造函数的创建,我认为注入的作用是创建出一个对象,这个对象里面的内容可以后续再传入,它更多的体现、或者我们需要注意的是 “分离关注点”

2.3 实现接口实例注入

因为接口没有构造函数,所以当我们想要依赖一些接口时,该怎么办。

我们来下面的示例,我们写一个 ​​Profession​​ 接口,代表职业:

interface Profession { fun doJob()}

假设我们有两个实现接口的类:医生类和程序猿类:

class Doctor : Profession{ override fun doJob() { Log.d("Doctor", "doctor do job") }}class Programmer : Profession{ override fun doJob() { Log.d("Programmer", "programmer do job") }}

这个时候我给 User 类添加一个职业的属性,并希望它能够自动注入:

class User @Inject constructor(var clothes: Clothes){ @Inject lateinit var profession: Profession}

因为 ​​Profession​​ 是一个接口,它有两个实现类,所以这样 Hilt 并不能知道我们要实例化哪个具体的实现类,所以编译的时候就会报错。

而 Hilt 也解决这种问题,首先我们要在每个实现类上注入构造函数:

class Doctor @Inject constructor() : Profession{ ...}class Programmer @Inject constructor() : Profession{ ...}

@Module@InstallIn(ActivityComponent::class)abstract class ProfessionModule { // 1 // 2 @Binds abstract fun bindDoctor(doctor: Doctor): Profession}

注释1: 我们写出来的类是一个抽象类,因为我们不需要具体的实现它,而且它没有具体的命名规则,因为我们也不会在代码中直接调用它,但是为了便于理解,我们起名一般叫 ​​接口名 + Module​​。

注释2: 我们假设该Module提供一个 Doctor 的职业,那我们需要定义一个 抽象方法 来获取一个Doctor类。 并且 该方法需要被 ​​@Binds​​ 注解修饰。这样就能被 Hilt 识别。

这样一来,我们就实现了接口的一个实例化的注入,我们来实验一下,在 User 中去展示它:

class User @Inject constructor(var clothes: Clothes){ @Inject lateinit var profession: Profession fun showMyself() { profession.doJob() }}// MainActivity@AndroidEntryPointclass MainActivity : AppCompatActivity() { @Inject lateinit var user: User override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) user.showMyself() }}

打印结果为:

可以看到我们的 Doctor 成功的注入了。

OK,我们了解了接口某一个实现类的注入 (Doctor),那假设这个时候,另外一个类需要注入接口的另一个实现类 Programmer,那我们是不是也得按照同样的做法,在 Module类中添加呢?

@Module@InstallIn(ActivityComponent::class)abstract class ProfessionModule { @Binds abstract fun bindDoctor(doctor: Doctor): Profession @Binds abstract fun bindProgrammer(programmer: Programmer): Profession}

这个时候发现运行,编译也会报错:

提示我们被绑定多次了。

这是因为 ​​Doctor​​​ 和 ​​Programmer​​ 都是相同类型,当他们一起被 Binds 注解,那 Hilt 不知道要去绑定哪一个。

这个时候就需要使用 ​​@Qualifier​​ 注解来帮助我们了,它就是为了这种 相同类型 依赖注入而产生的:

@Qualifier@Retention(AnnotationRetention.BINARY)annotation class BindDoctor@Qualifier@Retention(AnnotationRetention.BINARY)annotation class

我们创建了新的注解 ​​BindDoctor​​​ 和 ​​BindProgrammer​​​,他们都被 ​​@Qualifier​​​ 修饰,表示他们用来作用在同种类型上, ​​@Retention​​ 选择使用 BINARY类型,表明该注解保留到编译后,但无法通过反射来得到,是比较适合的注解。

接下来,我们要将这些注解作用在被 ​​Binds​​ 注解的抽象方法上:

@Module@InstallIn(ActivityComponent::class)abstract class ProfessionModule { @BindDoctor @Binds abstract fun bindDoctor(doctor: Doctor): Profession @BindProgrammer @Binds abstract fun bindProgrammer(programmer: Programmer): Profession}

class User @Inject constructor(var clothes: Clothes){ @BindProgrammer // 这次注入一个 Programmer @Inject lateinit var profession: Profession fun showMyself() { profession.doJob() }}

打印结果如下所示:

这下我们就实现了具体某个实例的注入啦。

2.4 实现第三方依赖注入

假设一些类不是由我们自己写的,而是由第三方库导入的。比如 ​​OkHttp​​ ,我们在使用网络请求的时候,需要使用它,为了分离关注点,我们需要对他进行依赖注入。

但是 ​​OkHttp​​​ 是我们不能修改的类,所以我们不能在它的构造函数上加入 ​​@Inject​​, 这个时候该怎么办呢?

@Module@InstallIn(ApplicationComponent::class)class NetModule { // 1 // 2 @Provides fun provideOkHttpClient(): OkHttpClient { // 3 return OkHttpClient.Builder().build() }}

注释3:new 一个对象,并返回。

这样,我们就能在我们代码中直接使用了:

@Inject lateinit var okHttpClient:

@Providers 的本质是什么? 第三方类因为其只读性,Hilt不能找到其构造函数,所以需要我们自己手动的创建,创建的方法被 ​​@Providers​​ 修饰, Hilt 找到这个方法,并提供由我们手动创建的对象。

所以 ​​@Providers​​ 的本质,是由我们自己创建对象, Hilt 帮我们注入。

现在大家都不会直接使用 OkHttp,而是使用 ​​Retrofit​​,所以我们来提供一个 Retrofit 把:

... @Provides fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit { return Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() }

因为 Retrofit的创建需要依赖一个 ​​OkHttpClient​​​ 对象,所以我们需要创建一个,但是我们也可以注入一个,因为我们之前已经有 ​​provideOkHttpClient​​,所以它就能提供一个实例,我们不用在担心什么了。

2.5 Hilt 的内置组件和作用域

2.5.1 @InstallIn 注解

我们之前看到了 ​​@InstallIn​​​ 这个注解,它的作用是用来表明 Module 作用的地方,它的参数时 ​​xxxComponent​​格式,前面xx代表作用域。

因为 Hilt 是Dagger的Android场景化,所以它能作用的地方和我们Android息息相关,有下面几处:

Application ->​​ApplicationComponent​​ViewModel ->​​ActivityRetainedComponent​​Activity ->​​ActivityComponent​​Fragment ->​​FragmentComponent​​View ->​​ViewComponent​​Service ->​​ServiceComponent​​View Annotation with​​@WithFragmentBindings​​​ ->​​ViewWithFragemntComponent​​

除了最后一个,别的作用域还是挺常见的。他们都要通过 ​​@InstallIn​​ 注入。

2.5.2 使注入对象单例

像 Retrofit 、 OkHttpClient 这样的全局都需要使用到的对象,我们希望它的作用域是全局,并且单例的。

但是 Hilt 提供的 ​​@Inject​​ 对象并不是单例的,每次 注入时都会重新生成一个新的实例,这就说明,假设我们要使用 Retrofit 来做网络请求, ​​@Providers​​ 每次提供的都是不一样的,这样对性能来说很不友好,而且不符合常规的逻辑设定。

Hilt 也有自己的解决方案,那就是使用 ​​@xxxScope​​​ 注解,它和上面的 ​​xxxComponent​​所对应,表示 在这个作用域内单例,来看看对应关系:

Application ->​​ApplicationComponent​​​ ->​​@Singleton​​ViewModel ->​​ActivityRetainedComponent​​​ ->​​@ActivityRetainedScoped​​Activity ->​​ActivityComponent​​​ ->​​@ActivityScoped​​Fragment ->​​FragmentComponent​​​ ->​​@FragmentScoped​​View ->​​ViewComponent​​​ ->​​@ViewScoped​​Service ->​​ServiceComponent​​​ ->​​@ServiceScoped​​View Annotation with​​@WithFragmentBindings​​​ ->​​ViewWithFragemntComponent​​​ ->​​@ViewScoped​​

因为 Application 是作用于全局,所以它的注解是 ​​@Singleton​​,比较好理解。

2.5.3 作用域的包含关系

2.6 Hilt 预置的 Qualifier

我介绍过 Hilt 是 Dagger针对Android的场景化,所以它低层做了很多事情,使得在Android上更好的使用。除了上面介绍过的那些注解外,还有很多别的东西,可以让我们去探索,同时也了解了Dagger本身。

Context 上下文是Android 独特的存在,它代表着 Application、Activity、Service的一些fwk层的东西。

而我们的代码中经常会需要 Context 来创建一些东西:

class A @Inject constructor(context: Context) { ...}

但是我们知道,它是系统类,我们无法注入 Context。那我们可以通过使用 ​​@Providers​​ 来创建吗?

@Providersfun provideContext() { -}

很明显,Context 是由AMS来创建的,我们无法直接创建一个上下文出来。这个问题该如何解决呢?

答案是:我们不用解决,Hilt 为我们提供了它自己预置的注解 ​​@ApplicationContext​​​ 和 ​​@ActivityContext​​,我们直接使用,Hilt会帮我们注入上下文。

class A @Inject constructor(@ApplicaitonContext context: Context)

而现在没有 ​​ServiceContext​​,可能是用的比较少吧?

​​@ApplicationContext​​ 提供的类型是 Application, 而不是我们自己的 App 自定义的 Application,加入我们要使用自己的该怎么办呢?答案是也很简单:

@Providersfun provideApplicaiton(application: Application): MyApplication { return applicaiton as MyApplication}

直接在 Module 中提供一个,并强转就 OK啦。

注意​​​ApplicationContext​​​ 的作用域是全局, 所以它修饰的类的作用只能是 ​​@InstallIn(Applicaiton)​​​ 或 ​​@Singleton​​,其他的也同理。

3 小结

之前没有用过 Dagger,因为项目不需要,且难学,问题多。 Hilt 出来之后解决了大部分的痛点,再不上车属实就有点说不过去了。

Hilt 相较与 Dagger,肯定是更好用,更适合Android来使用。 它和 ​​Koin​​的比较,只是性能上的差异,网上大部分的文章都认为 Hilt 性能更优,但是代码量更多,在大的项目使用 Hilt 会更好,而小的项目两者差别不会太大。具体还请开发者自己研究。

Hilt 作用是 提供依赖注入,帮助程序分离关注点,帮助搭建低耦合高内聚的框架,学习它,有利于我们学习 Android应用架构 方面的技能。

至于还有一些其他的用法,例如 ViewModel的注入,可以看下郭神、鸿洋等大佬的文章,现在使用Hilt的项目是越来越多了。

题外话: 因为项目庞大,我们团队一直没有迁到AndroidX,很多同学空有一身Jetpack本领却无处施展呀,不过AndroidX终将还是会来,我们今年将会迁移至AndroidX,届时将会上 Jetpack 全家桶,我学习 Hilt 也是基于这样的契机,我是完全没有 AndroidX 的开发经验,所以为了跟上大部队,就是补一些大佬的Blog、书籍等,将会在接下来的几个月学习Jetpack上的框架。

参考文章

​​Hencoder 从 Dagger 到 Hilt,谷歌为什么执着于让我们使用依赖注入?​​​​郭霖 Jetpack新成员,一篇文章带你玩转Hilt和依赖注入​​​​应用架构指南​​

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:Docker学习笔记之运行和管理容器
下一篇:点九图学习与制作
相关文章

 发表评论

暂时没有评论,来抢沙发吧~