美文网首页Android Other
依赖注入(DI) 和 Hilt

依赖注入(DI) 和 Hilt

作者: 慕尼黑凌晨四点 | 来源:发表于2023-03-03 09:19 被阅读0次

什么是依赖注入(dependency injection )

首先什么是依赖,这个很简单,我们编写一个Car类,Car类中需要并声明一个Engine类,这个Engine类就是Car类的依赖,当然也可以说Car类依赖于Engine类。
然后这个Engine类的创建有三种方式:

  1. 在Car类的内部,自己创建;
  2. 静态方法写入(如 Context getter 和 getSystemService());
  3. 由外部传进来,以参数或者构造方法中的形式提供。

其中第三种方式我们就称之为“依赖注入”。
也就是说“依赖注入”其实在我们日常的代码编写中很常见,比如我们的带参构造函数,建造者模式builder.xx(xx),工厂模式xxFactory.create(xx),甚至我们日常写的xxWidget中的setData() {mData = data},都是依赖注入。

为什么要试用依赖注入

设计模式六大原则中,有一个原则叫做依赖倒置原则

抽象不应该依赖于具体类,具体类应当依赖于抽象。

也就是说我们在代码中要尽量引用层次高的抽象类、接口来进行变量类型声明、参数类型声明、方法返回类型声明,以及数据类型的转换等,而不要用具体类来做这些事情。

换言之,要针对接口编程,而不是针对实现编程

这样做的好处是通过抽象来搭建框架,减少类与类之间的耦合性,以抽象搭建的系统要比以具体实现搭建的系统具有更高的扩展性,便于维护,同时也满足开闭原则的要求。

这里就不得不引入另一个名词叫控制反转(IOC Inversion Of Control),控制反转是根据依赖倒置原则衍生出来的一种编程思想。

用上面的例子来说:在Car中自己声明并创建Engine类,Engine类的控制权实际是在Car类的手中。但如果我们用依赖注入的方式将Engine类注入给Car的话,Engine类的实际控制权就不在Car的手中了,因为Car具体使用哪个Engine是由我们怎么传入Engine实例来决定的,如果用框架的话,Engine类的控制权就被框架所接管了。

而我们这里探讨的依赖注入,就是ioc思想下的一种具体的落地实现。具体的好处有:

重用组件: 因为我们在类外部构造依赖项;
组件解耦: 当我们需要修改某个组件的实现时,不需要在项目中进行大量变更;
易测试: 我们可以向依赖方注入依赖项的模拟实现,这使得依赖方的测试更加容易;
生命周期透明: 依赖方不感知依赖项创建 / 销毁的生命周期,这些可以交给依赖注入框架管理。

自动依赖注入库

从实现上,依赖注入框架可以归为两类:

  1. 基于反射的动态方案: Koin、Dagger、Guice;
  2. 基于编译时注解的静态方案(性能更高): Dagger2、Hilt。

Hilt是一个对Dagger库的扩展。Dagger 的名字取自有向无环图(DAG,Directed acyclic graph),起初是由Square公司开发的,当时采用的是运行时反射的方式,在2014年是过继给了Google公司接手,并完成了Dagger2.0的开发,改为了编译期生成代码的方式。

而Hilt也是Google基于Dagger开发的一个库,对Dagger做了Android场景化的处理,使之更符合我们Android开发者的使用习惯。(Dagger是没做场景区分的,可以用来开发后端、桌面端等,比如说Hilt则对Android studio做了适配,可以很方便的查看类之间的依赖关系)

Hilt的用法也相较而言更简单:

添加依赖:

...
plugins {
  id 'kotlin-kapt'
  id 'com.google.dagger.hilt.android'
}

android {
  ...
  // 需要启用java8
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  implementation "com.google.dagger:hilt-android:2.44"
  kapt "com.google.dagger:hilt-compiler:2.44"
}

// Allow references to generated code
kapt {
  correctErrorTypes true
}

然后 application中:

@HiltAndroidApp
class ExampleApplication : Application() { ... }

Hilt 目前支持以下 Android 类:

  • Application(通过使用 @HiltAndroidApp)
  • ViewModel(通过使用 @HiltViewModel)
  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

使用时用 @AndroidEntryPoint 为某个 Android 类添加注解,还必须为依赖于该类的 Android 类添加注解。例如,如果您为某个 fragment 添加注解,则还必须为使用该 fragment 的所有 activity 添加注解。

然后用@Hilt注解需要注入的字段。

注意:由 Hilt 注入的字段不能为私有字段。尝试使用 Hilt 注入私有字段会导致编译错误。

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var user: User
  ...
}

data class User constructor(val id: String, val name: String) {

    @Inject
    constructor() : this("0", "哈哈")
}

这里的user对象就被注入进去了。这只是Hilt简便的写法,标准的注入写法应该是这样的:

@Module
@InstallIn(SingletonComponent::class)
object UserModule {

    @Singleton
    @Provides
    fun providerUser() = User("2", "呵呵")
}

这个带 @Module 注解的类就称之为hilt模块,它会告知 Hilt 如何提供某些类型的实例。@Install 是告知 Hilt 每个模块将用在或安装在哪个 Android 类中。

比如 @InstallIn(ActivityComponent::class),就是把这个模块安装到Activity组件当中。那么在Activity、Fragment、View中是可以使用由这个模块提供的所有依赖注入实例。

@Singleton 是可选的,表示限定组件的作用域@Singleton 表示整个Application作用域内,只创建这一个对象。

Hilt一共提供了如下几种组件,并会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。

如果不写则表示未限定作用域,即每次注入都会创建一个新的实例。

Hilt组件 注入器面向的对象 作用域
SingletonComponent Application @Singleton
ActivityRetainedComponent Activity @ActivityRetainedScoped
ViewModelComponent ViewModel @ViewModelScoped
ActivityComponent Activity @ActivityScoped
FragmentComponent Fragment @FragmentScoped
ViewComponent View @ViewScoped
ViewWithFragmentComponent 带有 @WithFragmentBindings 注解的 View @ViewScoped
ServiceComponent Service @ServiceScoped

如果是接口也可以注入:

interface IUser {
    fun getUser(): User
}

class UserImpl @Inject constructor() : IUser {
    override fun getUser(): User {
        return User("3", "张三")
    }
}

@Module
@InstallIn(SingletonComponent::class)
interface UserModule {

    @Binds
    fun provideUser(impl: UserImpl): IUser
}

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {

  @Inject lateinit var userimpl: IUser
  ...
}

而且Hilt作为Google官方推荐的依赖库,是有Android studio 支持的。具体来说,当依赖注入的地方,是可以直接在Android studio中通过鼠标点击,快速跳转到原始注入的地方的。

其原理就藏在Dagger的名字中:DAG (directed acyclic graph):有向无环图

dagger

因为程序里的依赖关系拼接起来就是一个或者多个有向无环图:


dag2_example.png

正是因为这些依赖关系的存在,Android studio可以帮我们找到并跳转到依赖相应的位置,也正应如此,hilt可以在编译器就对我们构建的依赖关系进行检查,如果有存在环状的依赖关系,则会在编译期就报错给我们,也就是只要编译通过了就说明依赖关系已经没问题了。

实际使用效果

下面以dataSource为例,看下使用后的效果:

class MessageDataSource @Inject constructor(
    private val mMsgApi: MessageApi,
    @Dispatcher(NetSchedulers.IO) private val io: Scheduler,
    @Dispatcher(NetSchedulers.UI) private val ui: Scheduler
) : IMessageDataSource {

    override fun queryMessage(number: Int): Observable<HitokotoMsg> {
        return mMsgApi.getMessage("a")
            .subscribeOn(io)
            .observeOn(ui)
    }
}

interface MessageApi {
    @GET("/xx")
    fun getMessage(@Query("c") c: String): Observable<HitokotoMsg>
}
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class Dispatcher(val scheduler: NetSchedulers)

enum class NetSchedulers {
    IO, UI
}

@Module
@InstallIn(SingletonComponent::class)
class NetModule {

    @Dispatcher(scheduler = NetSchedulers.IO)
    @Provides
    fun providesIODispatcher(): Scheduler = Schedulers.io()

    @Dispatcher(scheduler = NetSchedulers.UI)
    @Provides
    fun providesUIDispatcher(): Scheduler = AndroidSchedulers.mainThread()
    
    @Provides
    fun provideMessageApi(retrofit: Retrofit): MessageApi {
        return retrofit.create(MessageApi::class.java)
    }

    @Provides
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .baseUrl("https://v1.hitokoto.cn/")
            .client(okHttpClient)
            .build()
    }

    @Provides
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            .readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
            .writeTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            .build()
    }

}

在整理代码逻辑的时候,我们只用关注类中用这个变量做了什么事,而不用关心变量具体是怎么来的,逻辑看着清爽了许多。想要看具体的依赖注入的来源,也可以利用Android studio的自动跳转功能一层层去查看,这些具体的依赖一般都放在XXModule中,看着也整洁了不少。

上面用@Qualifier注解标注的注解,这个注解就叫做限定符,是为了区分具体的依赖来源的。比如这里需要一个 Scheduler ,但这里我们提供了ui和io两个绑定,所以就需要自定义一个限定符注解加以区分。

Hilt 提供了一些预定义的限定符。例如来自应用或 activity 的 Context 类, Hilt 提供了 @ApplicationContext@ActivityContext限定符。

其实也可以用@Name 属性来区分,不过由于存在硬编码,所以不太推荐。

    @Provides
    @Named("default")
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            .readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
            .writeTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            .build()
    }

    @Provides
    @Named("loggingInterceptor")
    fun provideOkHttpClient2(): OkHttpClient {
        return OkHttpClient.Builder()
            .connectTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            .readTimeout(READ_TIMEOUT, TimeUnit.MILLISECONDS)
            .writeTimeout(CONNECT_TIMEOUT, TimeUnit.MILLISECONDS)
            .addInterceptor(HttpLoggingInterceptor())
            .build()
    }

    // 使用
    @Inject 
    @Named("loggingInterceptor")
    lateinit var okHttpClient: OkHttpClient

Hilt对象声明周期管理

上面的示例展示了Hilt两个方面的优势,代码的解耦和使用上的一些简化(用注解就能拿到对象,而不用到处new了)。Hilt还有个隐藏的优势就是对对象生命周期的管理。

场景一

比如说一个Activity中有两个Fragment,其中一个Fragment中有一个自定义View,在Activity的ViewModel中我们持有一个变量(又或者这两个Fragment之间有共享的变量)。如果我们想在自定义view中用上这个变量,要么得从Activity到Fragment,再到View,一层层塞进去;要么得从view往Fragment往Activity,写上两个接口一层层往上获取。

现在借助Hilt,我们有更简便的写法——直接注入。

场景二

想创造一个声明周期在Activity范围内的“单例对象”。

Hilt、Koin对比

Jetpack新成员,一篇文章带你玩转Hilt和依赖注入 ,
hilt工作原理 ,
从 Dagger 到 Hilt,谷歌为何执着于让我们用依赖注入?

相关文章

  • 依赖注入(DI) 和 Hilt

    什么是依赖注入(dependency injection ) 首先什么是依赖,这个很简单,我们编写一个Car类,C...

  • Android官方新推的DI库 Hilt

    Android官方新推的DI库 Hilt Hilt是Google Android官方新推荐的依赖注入工具.已加入到...

  • Jetpack - Hilt

    Jetpack - Hilt依赖注入、依赖注入框架Android 常用的依赖注入框架Hilt 的简单使用 1. 依...

  • 从 Dagger 迁移到 Hilt 可带来的收益

    Hilt 发布于 2020 年 6 月,为 Android 提供了依赖项注入 (DI) 的标准化方案。对于新项目,...

  • 依赖注入(Android):什么/为什么 DI?[第1部分]

    什么是 DI & Hilt? 根据文档,依赖注入是一种广泛用于编程的技术,非常适合 Android 开发。通过遵循...

  • Hilt使用姿势全解析

    Hilt是什么? Hilt是Android的依赖注入库,可以减少在项目中执行手动依赖项注入的样板代码。执行手动依赖...

  • Hilt入门

    Hilt 是什么 Hilt 是 Android 的依赖项注入库,可减少在项目中执行手动依赖项注入的样板代码。执行手...

  • 初识Spring架构

    对Spring的了解 依赖注入DI(Dependency injection) DI分为依赖和注入 那怎么将对象注...

  • Dagger2常用注解诠释

    依赖注入 控制反转(IoC)与依赖注入(DI)浅谈依赖注入理解依赖注入(IOC)和学习Unity Gradle配置...

  • Spring IOC容器

    由于Spring是采用依赖注入(DI)的方式来实现IOC,所以本文将IOC和依赖注入(DI)等同看待,主要讨论依赖...

网友评论

    本文标题:依赖注入(DI) 和 Hilt

    本文链接:https://www.haomeiwen.com/subject/kkvsldtx.html