Loading

重新学习Android——(七)Hilt

前言

本篇文章是我阅读Android官方的Hilt文档所写的笔记。因为Android官方文档实在是太晦涩难懂了,对于我这种已经好多年没碰Android,前置知识不够的人来说读起来真的煎熬,所以我打算每走一步都把自己的心得和理解写出来。

当然,我自己的理解有可能出现偏差,如果您发现了我这里的错误,欢迎指正。如果您有不同的见解也欢迎交流学习,共同进步。

如果害怕读到不正确的理解,那么推荐您还是去读官方文档,在文章最底部的参考部分有链接~~~

什么是依赖注入

千万别把依赖注入看的太死,依赖注入不过是一个用起来不错的设计模式而已,其实我们每天都在写依赖注入的代码。

class WordRepository(
  private val dao: WordDao // 一个接口
) {
  //...
}

上面的例子就是依赖注入。当一个类A内需要持有类B的实例时,就可以说类A依赖类B,或者类B是类A的一个依赖项。而依赖注入,不过是把一个类中的依赖项的加载过程放到了类外部,比如上面的WordRepository类允许外部通过构造器传入它的依赖——WordDao的实例。

依赖注入带来了什么好处

我认为依赖注入最大的好处就是让我们的代码的可测试性变得更强。

还是刚刚的例子,不过这次我们不使用依赖注入,而是将依赖的加载过程交由WordRepository自己,此时如果我们想对WordRepository进行单元测试就不太好办了。

class WordRepository() {
  private val dao: WordDao = application.getDB().getWordDao()
}

由于WordDao的实现已经在WordRepository中写死,它是通过数据库实例来获取的,如果卷入了数据库逻辑,那么我们进行的测试就不叫单元测试了,那叫集成测试。因为单元测试的目的是对代码中的最小单元的逻辑正确性进行测试,如果卷入外部逻辑,那么一旦测试过程中出现了问题,我们就不能确定这个问题一定是WordRepository的锅。

一般情况下,我们想要进行单元测试,就会把一个类中所有的外部依赖都替换成虚假的,比如这个WordDao,我们会替换成一个虚假的实现,它并不和数据库交互,只是简单的返回虚假的数据而已。这样就可以实现不卷入外部逻辑,只对WordRepository中的逻辑正确性进行测试。

但是上面写死的代码导致我们想要替换WordDao的实现就要修改WordRepository的代码,等到测试完毕,进行集成测试或上线的时候还要再修改WordRepository的代码,很麻烦,而且很容易出错。

// 单元测试时修改WordRespository的代码:
class WordRepository() {
  private val dao: WordDao = fakeWordDao()
}


// 集成测试时修改回来:
class WordRepository() {
  private val dao: WordDao = application.getDB().getWordDao()
}

而依赖注入把WordDao的加载这部分责任放到外部,这样进行单元测试时,我们只需要传入虚假的WordDao,在其他环境下我们传入其他实现就行,而不用来回修改WordRepository的代码。

// WordRepository并不关心WordDao到底是真的还是假的
class WordRepository(
  private val dao: WordDao
) {
  //...
}

另一个好处,是我个人的一点心得。我感觉依赖注入会让我们不自觉的定义接口,通过面向接口来编程就能够降低程序中各个组件之间的耦合。

Hilt简要介绍

知道了什么是依赖注入,那么像Hilt、Dagger这种依赖注入库的作用也就明了了,它们只不过是帮我们更加方便的实现依赖注入。一般的依赖注入库都提供了如下功能:

  1. 依赖项容器:用来管理所有的依赖项
  2. 自动扫描依赖项:自动扫描项目中定义的依赖项,并放到容器中
  3. 手动定义依赖项:手动定义一些依赖项,并放到容器中
  4. 自动注入依赖项:在程序的某些位置,如果需要依赖注入,那么依赖注入库会从容器中寻找有没有该依赖项,有的话就自动注入
  5. 依赖项生命周期管理:比如Singleton这种全局单例的依赖项、跟随特定类生命周期的依赖项、每次获取都重新创建的依赖项等。尤其是Android开发中,Android API SDK中的组件类(如Activity)都有自己的生命周期,而它们的依赖项往往也要跟随它们的生命周期来保证不出现内存泄漏

Hilt构建在依赖注入库Dagger库之上,Dagger学习曲线过陡,而且在Android开发中还是要写一些模板代码。Hilt提供了更便于Android开发者来使用的,简单的依赖注入方式。如果你学过Spring的依赖注入,我觉得应该很快可以掌握Hilt。

Dagger名字的秘密

Dagger的名字源于术语缩写DAG,也就是Directed Acyclic Graph——有向无环图。

我们一个程序是由各个组件构成的,组件之间存在依赖关系,而这些依赖关系可以用有向无环图来描述。

这是一个简陋的有向无环图,它描述了程序中有四个组件:A、B、C、D,其中A依赖B、D,B依赖C,C依赖D。

为什么是无环图

如果有环就循环引用了,请问你该如何构造下面的任意一个类的实例呢?

class A(
  private val b: B
)

class B(
  private val a: A
)

val b = B(
    A(
        B(
            A(
                B(
                    ....
                )
            )
        )
    )
)

添加Hilt依赖

在project的gradle文件中添加hilt的构建脚本

buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.40.5'
    }
}

在模块的gradle文件中添加hilt的依赖

implementation 'com.google.dagger:hilt-android:2.40.5'
kapt 'com.google.dagger:hilt-compiler:2.40.5'

在模块的gradle文件开启hilt的插件和kapt插件

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

启用Hilt

启用Hilt需要创建一个Application并在上面打上@HiltAndroidApp注解

@HiltAndroidApp
class HiltApplication : Application() {}

官方:@HiltAndroidApp会触发Hilt的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器。

别忘了在清单文件中使用这个Application

根据官方的说法,Hilt应该是针对Android中不同的组件提供了不同的依赖项容器,因为这些组件的生命周期不同。比如Application的生命周期总是从程序开始到结束,Activity则是从该界面创建到销毁。

依赖注入

通过在Android类上添加@AndroidEntryPoint注解可以让Hilt向该类注入它需要的依赖项。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject lateinit var analytics: AnalyticsAdapter

    // ...
}

比如这里,Android类MainActivity要求Hilt注入一个AnalyticsAdapter的实例,如果Hilt在对应的容器中能找到这个依赖,就会向该类注入,否则会报错。而关于什么是对应容器,一会有解释。

注意@Inject注解是javax包中定义的,不是Hilt的,Hilt没有这个注解,这是Java定义的依赖注入标准注解,大部分依赖注入框架都支持这个注解。

Hilt 目前支持以下 Android 类:

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

向Hilt中注册依赖项

知道了怎么向Android类中注入一个依赖,下一个问题就是我们的依赖项该如何添加到Hilt中。Hilt只是一个依赖注入框架,它不也不是神,它不可能知道MainActivity想要什么,并自己创建一个AnalyticsAdapter的实例注入进去。

constructor注册

这个功能基本对标Spring的@Component

你可以在一个类的构造器上添加@Inject注解,Hilt会发现这些类并将它们作为依赖项进行管理。

class AnalyticsAdapter @Inject constructor() {
    fun executeAnalytics() {
        println("executing analytics...")
    }
}

这里Hilt知道了AnalyticsAdapter类是程序中的一个依赖项,如果程序中有某些地方要求注入它,Hilt会自动创建一个AnalyticsAdapter的实例并注入。

可能有点晕,@Inject注解有了两个含义??

  1. 用于向类中注入依赖项
  2. 用于声明一个类是依赖项

为什么不用两个注解??其实是这样的,因为一个依赖项还可能依赖其他依赖项,比如:

class AnalyticsAdapter @Inject constructor(
    private val analyticsService: AnalyticsService
) {
    fun executeAnalytics() {
        analyticsService.analyticsMethods()
    }
}

AnalyticsAdapter依赖AnalyticsService,Hilt也会在依赖满足时自动注入。所以这里用@Inject完全没问题吧。(其实也不能说完全没问题,还是有点令人迷惑的,这一点Spring做的就很好)

手动注册

constructor注册简单好用,但是也有很多情况下它用不了。比如你想要作为依赖项的类不是你的。。。。你自然没办法跑到人家的类里在构造器上添加一个@Inject注解。同时,如果你想注入一个接口的实现类时,你也没法在接口上添加@Inject注解,因为接口没有构造器。

使用@Binds手动注册接口类型

如下,AnalyticsAdapter需要一个AnalyticsService作为依赖,但是AnalyticsService是一个接口。

class AnalyticsAdapter @Inject constructor(
    // 这里要求注入一个AnalyticsService
    private val analyticsService: AnalyticsService
) {
    fun executeAnalytics() {
        analyticsService.analyticsMethods()
    }
}

// 无法在接口上添加`@Inject`
interface AnalyticsService {
    fun analyticsMethods()
}

// 这里的`@Inject`只有当外部明确需要`AnalyticsServiceImpl`类型的依赖时Hilt才会创建并注入
class AnalyticsServiceImpl @Inject constructor() : AnalyticsService {

    override fun analyticsMethods() {
        Log.i("AnalyticsServiceImpl", "analytics methods...")
    }

}

此时,我们需要使用一个Hilt模块,Hilt模块也是定义依赖项的地方,只不过它提供了更加复杂灵活的定义方式。我们需要使用一个Hilt模块来说明,当外部代码需要一个AnalyticsService实例时,注入某一个实现类,本例子中是AnalyticsServiceImpl

@Module   // 说明是Hilt模块
@InstallIn(ActivityComponent::class) // 项目中的所有Activity可以被注入该模块中的依赖
abstract class AnalyticsModule1 {
    @Binds
    abstract fun bindAnalyticsService(
        analyticsServiceImpl: AnalyticsServiceImpl
    ): AnalyticsService
}

在一个抽象方法上使用@Binds注解就是定义了一个依赖项,我们可以这样理解它:

  1. 返回值类型定义了这是一个AnalyticsService类型的依赖项,当外部依赖AnalyticsService类型时可以注入该实例
  2. 这个依赖项的参数说明了该依赖项依赖AnalyticsSerivceImpl实例,这个依赖可以满足并被Hilt自动注入,因为AnalyticsServiceImpl的构造器上添加了@Inject
  3. Hilt会生成这个抽象方法的代码,这个抽象方法就是简单的将参数中的依赖项返回,因为AnalyticsServiceImplAnalyticsService的子类,自然可以返回

你当然也可以简单的理解成@Binds应用在一个抽象方法上,Hilt会在所有依赖该方法返回值类型实例的地方注入该方法参数类型实例,这大致是官方文档的意思。但是我本人觉得上面的理解可以与依赖注入框架的工作方式包括其它使用方法保持一致。

和Spring中的不一致

没学过Spring的跳过这个地方!!!!!!!!!!否则可能会更加迷惑!!!!

Spring中允许在需要接口或父类型作为依赖时直接注入子类实例,而Hilt貌似不允许!!!至少现在来看,你想注入什么类型,这个类型从哪来,必须写的明明白白。

有好处也有坏处吧,好处就是潜在的问题更少了,坏处就是使用起来复杂了。

使用@Provides手动注册第三方库中的类

下面看看,如果我们要将第三方类库中的类注册为依赖项该咋办。

interface AnalyticsService {
    fun analyticsMethods()
}

class AnalyticsServiceImpl : AnalyticsService {

    override fun analyticsMethods() {
        Log.i("AnalyticsServiceImpl", "analytics methods...")
    }
}

现在就假装AnalyticsServiceImpl是第三方类库中的类,没法在它的构造器上添加@Inject,那么自然没法通过@Binds来提供这个实现类的依赖项了,因为@Binds要求AnalyticsServiceImpl本身也是一个依赖项。

这时如果你还用@Binds,那么就会无法通过编译

可以使用@Provides解决:

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

    @Provides
    fun provideAnalyticsService(): AnalyticsService {
        return AnalyticsServiceImpl()
    }

}

@Provides定义了一个依赖项,同样,可以这样理解:

  1. 方法的返回值类型定义了该依赖项的类型,当外界需要注入AnalyticsService类型的依赖时,即可注入
  2. 方法体定义了应该如何创建这个依赖项的实例,没用Hilt帮我们创建,所以我们并不用在这个类上添加@Inject注解

这东西有点像工厂模式是吧,看一个真正的定义第三方库中的依赖项的例子:

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {

  @Provides
  fun provideAnalyticsService(
    // Potential dependencies of this type
  ): AnalyticsService {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

下面我们将AnalyticsAdapter依赖项的注册也放到模块中:

@Provides
fun provideAnalyticsAdapter(
    analyticsService: AnalyticsService
): AnalyticsAdapter {
    return AnalyticsAdapter(analyticsService)
}
  1. 返回值说明该依赖项是一个什么类型的依赖项
  2. 参数说明该依赖项依赖那些其他依赖项,由于我们前面已经定义了AnalyticsService类型的依赖项,这个依赖项会被Hilt自动注入
  3. 方法体指明该如何创建该依赖项

@Binds@Provides的总结

这两个注解都用来定义依赖项,但不管是@Binds还是@Provides,它们都是这样的,非常一致的形式:

  1. 通过返回值说明该依赖项的类型
  2. 通过参数传入该依赖项依赖的其他依赖项

这也是为什么我那样理解(而不是跟从官方的解释)@Binds的原因

为一个类型提供多个绑定

我们可以为同一个类型提供多个依赖项,而且这是项目中很常见的需求,难道项目中有两个AnalyticsService的实现类很奇怪吗??

两个实现类:

class AnalyticsServiceImpl : AnalyticsService {

    override fun analyticsMethods() {
        Log.i("AnalyticsServiceImpl", "analytics methods...")
    }
}

class AnthorAnalyticsServiceImpl : AnalyticsService {
    override fun analyticsMethods() {
        Thread.sleep(1000L)
        Log.i("AnthorAnalyticsServiceImpl", "anthor analytics methods...")
    }
}

Hilt模块中注册依赖

@Provides
fun provideAnalyticsService(): AnalyticsService {
    return AnalyticsServiceImpl()
}

@Provides
fun provideAnthorAnalyticsService() : AnalyticsService {
    return AnthorAnalyticsServiceImpl()
}

现在你运行程序绝对会报错:

明显是因为我们有多个AnalyticsService类型的依赖,如果有人需要注入,Hilt该注入哪个??这不是它能决定的,这根随我们的业务逻辑。

不过你可以使用限定符!!!如下是几个类

class 水果 {}

class 苹果 : 水果() {}
class 香蕉 :水果() {}

下面就是限定符的使用,限定符主要用于说明两个同样类型的依赖项各自的特点,然后外部可以根据这个特点来决定我要哪个依赖项

@圆形的还不用扒皮就能吃
@Provides
fun 提供苹果() : 水果 {
  return 苹果()
}

@长条的又能吃又能用
@Provides
fun 提供香蕉() : 水果 {
  return 香蕉()
}

// ... 外部使用
class 我 {
  @Inject 
  @长条的又能吃又能用
  lateinit var 最喜欢的水果: 水果 // 肯定是香蕉被注入
}

所以我们可以定义两个实现类各自的描述符:

// 比较慢的分析服务
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class SlowAnalyticsService { }

// 快速的分析服务
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class QuickAnalyticsService { }

Hilt模块:

@QuickAnalyticsService
@Provides
fun provideAnalyticsService(): AnalyticsService {
    return AnalyticsServiceImpl()
}

@SlowAnalyticsService
@Provides
fun provideAnthorAnalyticsService() : AnalyticsService {
    return AnthorAnalyticsServiceImpl()
}

在需要注入依赖的地方使用描述符

@Provides
fun provideAnalyticsAdapter(
    @QuickAnalyticsService // 选择快速的分析服务进行注入
    analyticsService: AnalyticsService
): AnalyticsAdapter {
    return AnalyticsAdapter(analyticsService)
}

预定义限定符

Hilt提供了一些预定义的限定符。例如,由于您可能需要来自应用或ActivityContext类,因此Hilt提供了 @ApplicationContext@ActivityContext限定符。

假设本例中的AnalyticsAdapter类需要Activity的上下文。以下代码演示了如何向AnalyticsAdapter提供 Activity上下文:

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }

上下文对象由Hilt自动注入,啥都不用咱们管。卧槽好爽啊!!!!!

为Android类生成的组件

组件——Component,应该是Dagger中的概念,大致应该就是依赖项实例的管理器?不太清楚。但是Hilt的文档上是这样说的:

不像传统的Dagger,Hilt用户永远不需要直接的定义或实例化Dragger Component,Hilt提供了为你生成的预定义Component。Hilt有一系列内建的Component集合(和相应的作用域注解),它们自动的与Android应用的多种生命周期整合。

在模块中可以像前面一样使用@InstallIn注解为模块绑定模块作用的组件。

组件生命周期

Hilt 会按照相应 Android 类的生命周期自动创建和销毁生成的组件类的实例。

自己的理解:Hilt会为每个@AndroidEntryPoint(或@HiltAndroidApp创建并绑定一个自己对应的组件类。说白了就是每一个Android类(Application、Activity、Fragment等)都会有一个自己的组件类实例,它们随着自己的生命周期建立或消亡。这么看Dagger中的组件就是Spring中说的容器??

组件作用域

其实我早观察到了一个现象,但我没有放在笔记里说,我想官方文档后面总会有解释这个现象的内容,这就来了。

这个现象就是我们每次通过@Inject注入一个依赖的时候,我发现,我们总是得到一个新的对象。

我在MainActivity中注入了两次AnalyticsAdapter,我得到了两个实例:

这是Hilt的默认行为,因为Hilt中的默认绑定(注入)都未限定作用域,也就是每次都新建一个依赖项实例。不过你可以通过一些注解将绑定的作用域限定到某一(Android类生命周期的)作用域。

这图看着很迷,该怎么阅读这个图?

先看最右面一列,右面一列是依赖项的作用域注解,你可以把它放到任何定义依赖项的地方,然后这个依赖项就具有了此作用域。中间一列是该依赖项该在哪个组件中是唯一的。比如标注了@Singleton的依赖项在ApplicationComponent组件中唯一,意思就是在整个Application内,你怎么要求注入,被注入的都是一个对象,再比如@ActivityScope,依赖项会在ActivityComponent中,而每一个Activity都有独立的ActivityComponent,所以你在同一个Activity内,你怎么要求注入注入的都是同一个对象,而你在两个不同的Activity中就不是同一个对象。

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
    //声明依赖项作用域
    @ActivityScoped
    @Provides
    fun provideAnalyticsAdapter(
        /* 这里是AnalyticsService的可选依赖 */
        @ActivityContext context: Context,
        @QuickAnalyticsService
        analyticsService: AnalyticsService
    ): AnalyticsAdapter {
        return AnalyticsAdapter(context, analyticsService)
    }

}

然后MainActivity中被注入的就是同一个对象了

别忘了构造器注册依赖项,在这里也同样可用

@ActivityScoped
class AnalyticsAdapter @Inject constructor(
  private val service: AnalyticsService
) { ... }

模块中的作用域

假设你在模块中定义依赖项的作用域,模块绑定到的作用域(@InstallIn中的组件作用域)必须和依赖项的作用域相同

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
    //声明依赖项作用域
    @ActivityScoped // 这里不能是@Singleton或其它的
    @Provides
    fun provideAnalyticsAdapter(
        @ActivityContext context: Context,
        @QuickAnalyticsService
        analyticsService: AnalyticsService
    ): AnalyticsAdapter {
        return AnalyticsAdapter(context, analyticsService)
    }

}

组件层次结构

将模块安装到组件后,其绑定就可以用作该组件中其他绑定的依赖项,也可以用作组件层次结构中该组件下的任何子组件中其他绑定的依赖项:

使用@InstallIn将模块安装到对应Component后,该模块中的依赖项就可以依赖该Component中的其他依赖,尽管它们不在同一个模块中定义。

比如我们可以在另外一个也是被安装到ActivityComponent组件中的模块依赖之前被安装到同个作用域的模块中的依赖项。

@Module
@InstallIn(ActivityComponent::class)
object TestModule {
    @ActivityScoped
    @Provides
    fun provideAnalyticsAdapterName(
        @ActivityContext context: Context,
        adapter: AnalyticsAdapter
    ): String {
        return adapter.toString()
    }

}

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {
    // ...忽略一些依赖项定义...
    @ActivityScoped
    @Provides
    fun provideAnalyticsAdapter(
        /* 这里是AnalyticsService的可选依赖 */
        @ActivityContext context: Context,
        @QuickAnalyticsService
        analyticsService: AnalyticsService
    ): AnalyticsAdapter {
        return AnalyticsAdapter(context, analyticsService)
    }

}

同时,比当前Component作用域更大的模块中的依赖项可以被作用域更小的模块中的依赖项依赖。反过来则不行。

就是,被安装到SingletonComponent中的模块中的依赖项可以被安装到ActivityComponent中的模块中的依赖项依赖,而返回来不行。

这是很自然的一件事,比如如下的用法就不行:

@Module
@InstallIn(SingletonComponent::class)
object TestModule {
    @Singleton
    @Provides
    fun provideAnalyticsAdapter(
        @ApplicationContext context: Context,
        @QuickAnalyticsService
        analyticsService: AnalyticsService
    ): String {
        return analyticsService.toString()
    }

}

AnalyticsServiceActivityComponent中的依赖,它不能被SingletonComponent依赖。

我们可以这样理解,对于上层Component,每下一层,这些依赖都更加具体,上层不知道该依赖哪个Activity的ActivityComponent中的依赖项。

对于Spring开发者

总结了一些Hilt中的概念和Spring中概念的对应关系

  • @Inject -- @Autowired
  • 在构造器上的@Inject -- @Component
  • @Module -- @Configuration
  • @Binds和@Provides -- @Bean
  • @Quailfier -- @Quailfier
  • Component -- 容器 (不确定)

最后

由于网上的各种文章少之又少,官方文档也是没有透露太多信息,如果想要详细的了解Hilt中的每一个细节,我觉得一定得把Dagger学会。

如有错误欢迎指正

参考

posted @ 2022-01-11 14:45  yudoge  阅读(393)  评论(1编辑  收藏  举报