从 Compnent Tree 视角看 Dagger 到 Hilt 的演变

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第1张图片

本文视频:https://www.bilibili.com/video/BV1cg4y1w7Vh/

1. 从 Dagger 的本质说起

一言以蔽之, Dagger 的本质就是一棵 Component Tree

1.1 Component :依赖注入容器

component 是 Dagger 中的核心概念,我们通过 @Component 注解定义并生成代码。component 作为依赖注入容器,身兼工厂、仓库、物流三种角色于一身。Dagger 中的很多重要注解也是服务于它的这三个身份:

  • @Module@Provides 为 comopnent 安装了生产依赖对象所需的“工厂”;
  • @Singleton 等作用域注解将依赖以单例形式存储在 component 这个“仓库”中,被更多地方共享;
  • @Inject 为 component 提供送货上门的“物流”的能力,标记被注入的目标的字段,将依赖注入其中。

以下是使用 Dagger 定义的 ApplicationComponent ,为它为 App 注入所需的 userRepo 成员。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第2张图片

1.2 Tree:对应用层级关系的反映

一个应用往往都有层级结构。例如一个 Android 项目中从上到下有 Application -> Activity -> Fragment 等多层,每层可访问对象的生命周期长度不同:

  • UserRepository :服务整个 Application
  • LoginViewModel :只在 LoginActivity 范围可见。

当我们使用 Dagger 来管理这些依赖对象时,需要有相对应的 component 提供不同“保鲜期”的仓库。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第3张图片

此外,由于依赖对象之间有依赖关系。例如 LoginViewModel 需要使用 UserRepository,因此对应 component 也产生了继承关系,LoginActivityComponent 依赖 ApplicationComponent 的实例来构建自己的实例,component 之间形成父子关系,进而构成一棵组件树。

2. 使用 @Subcomponent 构建组件树

Dagger 使用 @Subcomponent 定义子组件进而形成组件树。

2.1 定义子组件

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第4张图片

在组件的定义上,@Subcomponent@Component 没有区别,需要以此声明组件依赖的 module(非必须),注入的目标,以及创建子组件所需要的工厂。此外还需要一个自定义作用域注解 @ActivityScope,它是 @Scope 的派生类,表明当前子组件的生命周期。

2.2 建立组件父子关系

子组件不能直接关联父组件,需要借助 Module 安装到父组件。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第5张图片

如上,通过 SubcomponentsModule 子组件 LoginActivityComponent 被安装到父组件 ApplicationComponent 中,同时父组件中声明了子组件的工厂,意味着父组件可以创建子组件。

2.3 使用子组件注入

因为组件之间有继承关系,子组件或需要依赖父组件构建自己的示例。因此,我们不能凭空构建子组件,需要通过父组件来构建,建立内在依赖关系

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第6张图片

如上,在合适的时机,LoginActivity 通过父组件提供的工厂创建子组件,完成对自身的注入。

2.4 Boilerplate 问题

前面的介绍可以感受到,@Subcomponent 构建组件树过程中带来了比较多的模板代码:

  • 子组件中提供 Subcomponent.Factory
  • 父组件中需要声明子组件的工厂
  • 合适的时机通过父组件创建子组件完成注入。

随着项目中的 activity 、fragment 等越来越多,上述类似的代码会反复出现,影响大家使用 Dagger 的积极性。为了解决这个问题 dagger.android 和 Hilt 相继问世。

3. Dagger.android:代码生成组件树

dagger.android 是 Dagger 针对 Android 项目推出的子项目,核心思想是通过代码生成 subcomponent,降低 Andorid 项目中的模板代码。dagger.android 是独立于 Dagger 的库,工程中需要单独依赖:

//raw dagger2
implementation 'com.google.dagger:dagger:2.x'
kapt 'com.google.dagger:dagger-compiler:2.x'

//dagger.android
implementation 'com.google.dagger:dagger-android:2.x'
implementation 'com.google.dagger:dagger-android-support:2.x'
kapt 'com.google.dagger:dagger-android-processor:2.x'

我们看一下引入 dagger.android 后的效果:

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第7张图片

关键变化是新增了 @ContributesAndroidInjector 注解,它标记了一个返回值为 LoginActivity 的方法,其含义是编译期生成 LoginActivity 对应的子组件。因此我们无需再显示地通过 @Subcomponent@Subcomponent.Factory 声明子组件了,SubcomponentsModule 中也无需添加 subcomponents 依赖。

3.1 @ContributesAndroidInjector 生成子组件

看一下 @ContributesAndroidInjector 生成的完整代码:

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第8张图片

可以看到我们少写的代码,这里都生成了。LoginActivitySubcomponent 是子组件, SubcomponentsModule_ContributesLoginActivity 是用来安装子组件的 module。

3.2 DispatchingAndroidInjector 提供组件映射

特别值得一提的是代码中有一个 bindAndroidInjectorFactory 方法并携带了 @IntoMap, @ClassKey 等若干注解,它们可以编译期 Dagger 构建依赖链条的过程中,向 DispatchingAndroidInjector 类填充一个 map,即其 injectorFactories 成员:

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第9张图片

透过 map 的泛型定义不难推测,它是 Android class 与其对应子组件的工程的映射表,具体到前面 LoginActivity 的例子中,会填入 LoginActivity.class to LoginActivitySubcomponent.Factory 到 map 中

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第10张图片

我们在 App 中声明 androidInjector 成员,并通过 Dagger 注入 DispatchingAndroidInjector 实例。App 通过 HasAndroidInjector 接口对外宣称自己持有一个 androidInjector,可以为各个 Activity 提供注入。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第11张图片

如上,在 LoginActivity 中,我们不再需要通过父组件的工厂创建子组件,调用一个 AndroidInjection.inject 静态方法即可完成注入。静态方法内部会向上寻找 HasAndroidInjector,然后通过映射表创建注入所需的子组件。

当然享受便利的同时,也要付出义务,LoginActivity 也需要实现 HasAndroidInjector,并声明 androidInjector,向下为它的 fragment 们提供注入。

3.4 dagger.android 的问题

dagger.andriid 主要做了下面两件事,帮我们减少了模板代码 :

  • 通过 @ContributesAndroidInjector 生成 subcomponent 及其 factory,省去了我们显示地定义子组件,父组件也不需要再声明 Subcomponent.Factory
  • 让 Android 各层级对象持有 AndroidInjector,通过静态方法完成对低层级对象的注入,省去了显示地创建子组件完成注入

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第12张图片

dagger.android 虽然做出上述改善,但是代价是引入了一些新的模板代码 :

  • 需要配置 @ContributesAndroidInjector
  • Android 组件需要实现 HasAndroidInjector 接口,并注入 AndroidInjector 成员
  • 需要手动调用 AndroidInjection.inject

4. Hilt:预定义组件树

dagger.android 没有存在模板代码,所以诞生了 Hilt,后者的思想是通过 “预定义” 的方式彻底消灭模板代码。

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

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

4.1 预定义 Component

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第13张图片

相对于 dagger.android 帮我们生成 LoginActivitySubComponent, Hilt 中索性不允许自定义的 subcomponent,提供了预定义的 ActivityComponent 作为所有 activity 共享的提供注入的组件。而 LoginActivityModule 等原本安装到 LoginActivitySubComponent 的依赖,通过 @installIn 注解安装到 ActivityComponent 中。

ActivityComponent 是个 interface,编译期生成实现类 ActivityC

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第14张图片

modules 中可以看到各 activity 依赖的 XXXActivityModule 都被 instllInActivityC 中,ActivityC 可以为所有 activity 提供注入。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第15张图片

FragmentCBuilderModuleViewCBuilderModule 用来安装 Hilt 另外两个预定义组件 FragmentComponentViewComponent,Hilt 为 Android 中的关键概念都提供了对应的预定义组件,且将它们建立树行关系。

4.2 预定义 Inject

dagger.android 通过提供静态方法注入降低了 inject 的成本。而在 Hilt 中,inject 的成本趋近于零,只需要在 activity 等 Android 组件添加 @AndroidEntryPoint 注解,其他什么都不用做。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第16张图片

前面看到 ActivityC 实现了 XXXActivity_GeneratedInjector 接口,这些接口就是 @AndroidEntryPoint 的产物

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第17张图片

LoginActivity_GeneratedInjector 提供了面向 LoginActivityinject() 方法,并通过 @EntryPoint 安装到 ActivityComponent 中,这样编译后 ActivityC 就具备了 injectLoginActivity 的能力。

@EntryPoint 是 Hilt 的重要注解,因为我们没法在 Hilt 的预定义 Component 中添加 inject 方法,所以当我们希望 Hilt 为自定义类提供注入时,可以自定义 inject 接口,通过 @EntryPoint 安装到 Hilt 的预定义组件中。 @AndroidEntryPoint 只不过是针对 Android 类提前生成了 @EntryPoint 代码。

那么 LoginActivity 是什么时候调用 ActivityCinjectLoginActivity 完成自身注入的呢?Hilt 会为 LoginActivity 生成一个 activity 的派生类,它在合适的时间点调用 inject(),内部会调用 ActivityC#injectLoginActivity

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第18张图片

而 Hilt 通过 Transform + ASM,让编译后 LoginActivity 继承了 Hilt_LoginActivity,这样就可以在不写任何代码的情况下,让 LoginActivity 基于 Hilt 的 ActivityC 完成注入,即所谓的 “预定义 inject”。

4.3 预定义 @Scope

Dagger 在 Android 中使用时,往往需要需要自定义作用域注解表明不同 Android 类的生命周期。Hilt 伴随着预定义组件,也提供了与之对应的预定义作用域注解

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第19张图片

例如,添加了 @ActivityScoped 注解,表示 provides 的对象在 ActivityComponent 范围内以单例存在。

Hilt 为所有的关键的 Android 类都提供了预定义组件和相对应的预定义作用域注解,所以也可以说 Hilt 对整棵组件树进行了预定义:

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第20张图片

5. 渐进式迁移

通过前面介绍,我们能感受到 Hilt 对 Dagger 的 Boilerplate 问题进行了比较彻底的改进,建议大家尽快升级到 Hilt。从组件树的视角来理解 Dagger 与 Hilt 的区别,可以帮助我们完成渐进式的升级。

最安全的升级过程就是从沿着组件树的树干,按照 Application -> Activity -> Fragment -> ... 的顺序,将 Dagger 的自定义组件合并到 Hilt 的预定义组件,最终实现依赖注入完全托管到 Hilt 树。

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第21张图片

5.1 将 Dagger 组件合并到 Hilt

我们以 ApplicationComponent 为例看一下,看一下 Dagger 中自定义 ApplicationComponent 如何合并到 Hilt 的预定义的 SingletonComponent

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第22张图片

合并后的代码如下所示,核心是 @EntryPoint 的使用:

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第23张图片

  • @EntryPointSingletonC 实现了 ApplicationComponent 接口,代码中其他依赖 ApplicationComponent 的地方可以无缝切换到 SingletonComponent,两棵树在根节点完成合并。
  • 新定义一个 module,通过 includes 将原本 ApplicationComponent 的依赖打包安装到 SingletonComponent

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第24张图片

如上,在 App 中添加 @HiltAndroidEntryPoint 注解,Hilt 可以为 App 提供注入服务。但是 App 的 component 成员不能立即删除,可能还有其他代码在引用它。但是在根节点合并后,我们可以通过 EntryPointAccessors 从 Hilt 获取 ApplicationComponent 的实现。

其他层级的组件,例如 ActivityComponent, FragmemtComponent 等,也可以仿照 ApplicationComponent 向 Hilt 做合并。如果你使用的是 dagger.android,则不再需要 @ContributesAndroidInjector 生成预定义组件了,可以删除相关代码,将依赖的 module 安装到 Hilt 的对应组件,如下:

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第25张图片

5.2 清理 Dagger 残留代码

当我们将组件树上的所有组件都合并到 Hilt,Android 类都转向通过 Hilt 获取依赖注入,所以 Dagger 或者 dagger.android 相关的代码都可以清理掉了

从 Compnent Tree 视角看 Dagger 到 Hilt 的演变_第26张图片

如上,以 App 为例,所以 Dagger 或者 dagger.android 相关的注入代码都可以删除了,代码清爽多了。

6. 预定义组件的问题

Hilt 的预定义组件降低复杂度的同时,也丧失一些自定义组件的灵活性,我们来看几个常见问题

6.1 区分具体 activity 类型

Dagger 依赖图中可能有对当前具体 activity 类型的依赖。通常我们像下面这样,在创建 LoginActivityComponent 时为 Dagger 传入 activity 实例。

@ActivityScope
@Subcomponent
interface LoginActivityComponent {
    @Subcomponent.Factory
    interface Factory {
        fun create(@BindsInstance activity: LoginActivity): LoginActivityComponent
    }
}

Hilt 中没有机会创建自定义组件,该如何提供不同类型的 activity 依赖呢。

预定义组件 ActivityComponent 中默认提供了当前 activity 的依赖,但是不区分具体类型,我们可以通过不同类型的 module 强转 activity 为具体类型后提供出去,代码如下:

@InstallIn(ActivityComponent::class)
@Module
class LoginActivityModule {
    @Provides
    fun providesLoginActivity(activity: Activity): LoginActivity> =
        activity as? LoginActivity?
}

但是必须说一句,对具体 activity 类型的依赖并非一个好设计,这意味着 activity 可能违反了单一职责的设计原则。

6.2 根据目标 activity 提供同一接口的不同实现

比如下面代码中,我们可以为不同的 activity 组件提供不同 module,从而提供 LoginService 的不同实现。

interface LoginActivityModule {
    
    @ActivityScope
    @ContributesAndroidInjector(modules = [EmailModule::class])
    fun contributesEmailLoginActivity(): EmailLoginActivity
    
    @ActivityScope
    @ContributesAndroidInjector(modules = [PhoneModule::class])
    fun contributesPhoneLoginActivity(): PhoneLoginActivity

}

@Module
interface EmailModule {
    @Binds
    fun bindsService(service: EmailLoginService): LoginService
}

@Module
interface PhoneModule {
    @Binds
    fun bindsService(service: PhoneLoginService): LoginService
}

而 Hilt 中,两个 module 都会安装到同一个预定义组件 ActivityComponent 中,LoginService 也只能有一个实现。解决办法跟前面类似,也是根据当前 activity 类型,动态返回不同的 LoginService

@InstallIn(ActivityComponent::class)
@Modules
class LoginModule {
    
    @Provides
    fun providesService(activity: Activity): LoginService =
        when(activity) {
            is EmailLoginActivity -> EmailLoginService()
            is PhoneLoginActivity -> PhoneLoginService()
            else -> error("Invalid Activity")
        }
}

7. 总结

当我们认清了 Dagger 的本质是一颗组件树这一事实之后,可以更好地理解 dagger.android 和 Hilt 诞生的目的,都是通过不同方式降低组件树的构建成本,前者选择了代码生成的方式,后者选择了预定义的方式。

Hilt 的预定义组件虽然牺牲了一定的灵活性,但是最大限度的降低了组件树的构建成本。如果你想引入 DI 框架但是一直苦恼于 Dagger 的使用成本,那么 Hilt 一定能满足你的需求,快用起来吧~

本文视频:https://www.bilibili.com/video/BV1cg4y1w7Vh/

你可能感兴趣的:(dagger,Hilt,Android,dagger,hilt,android,jetpack,android)