1. 从 Dagger 的实质说起

一言以蔽之, Dagger 的实质便是一棵 Component Tree (组件树)

1.1 Component :依靠注入容器

component 是 Dagger 中的中心概念,咱们经过 @Component 注解界说并生成代码。component 作为依靠注入容器,身兼工厂、库房、物流三种角色于一身。Dagger 中的许多重要注解也是服务于它的这三个身份:

  • @Module@Provides 为 comopnent 装置了生产依靠方针所需的”工厂”;
  • @Singleton 等效果域注解将依靠以单例形式存储在 component 这个“库房”中,被更多当地同享;
  • @Inject 为 component 供给送货上门的“物流”的才能,标记被注入的方针的字段,将依靠注入其间。

以下是运用 Dagger 界说的 ApplicationComponent ,为它为 App 注入所需的 userRepo 成员。

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

1.2 Tree:对运用层级联系的反映

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

  • UserRepository :服务整个 Application
  • LoginViewModel :只在 LoginActivity 规模可见。

当咱们运用 Dagger 来办理这些依靠方针时,需求有相对应的 component 供给不同“保鲜期”的库房。

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

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

2. 运用 @Subcomponent 构建组件树

Dagger 运用 @Subcomponent 界说子组件进而形成组件树。

2.1 界说子组件

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

在组件的界说上,@Subcomponent@Component 没有差异,需求以此声明组件依靠的 module(非必须),注入的方针,以及创立子组件所需求的工厂。此外还需求一个自界说效果域注解 @ActivityScope,它是 @Scope 的派生类,标明当时子组件的生命周期。

2.2 树立组件父子联系

子组件不能直接相关父组件,需求凭借 Module 装置到父组件。

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

如上,经过 SubcomponentsModule 子组件 LoginActivityComponent 被装置到父组件 ApplicationComponent 中,一起父组件中声明晰子组件的工厂,意味着父组件能够创立子组件。

2.3 运用子组件注入

由于组件之间有承继联系,子组件或需求依靠父组件构建自己的示例。因而,咱们不能随便构建子组件,需求经过父组件来构建,树立内涵依靠联系

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

如上,在合适的时机,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 后的效果:

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

要害改变是新增了 @ContributesAndroidInjector 注解,它标记了一个回来值为 LoginActivity 的办法,其意义是编译期生成 LoginActivity 对应的子组件。因而咱们无需再显现地经过 @Subcomponent@Subcomponent.Factory 声明子组件了,SubcomponentsModule 中也无需增加 subcomponents 依靠。

3.1 @ContributesAndroidInjector 生成子组件

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

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

能够看到咱们少写的代码,这里都生成了。LoginActivitySubcomponent 是子组件, SubcomponentsModule_ContributesLoginActivity 是用来装置子组件的 module。

3.2 DispatchingAndroidInjector 供给组件映射

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

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

透过 map 的泛型界说不难估测,它是 Android class 与其对应子组件的工程的映射表,详细到前面 LoginActivity 的例子中,会填入 LoginActivity.class to LoginActivitySubcomponent.Factory 到 map 中

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

咱们在 App 中声明 androidInjector 成员,并经过 Dagger 注入 DispatchingAndroidInjector 实例。App 经过 HasAndroidInjector 接口对外宣称自己持有一个 androidInjector,能够为各个 Activity 供给注入。

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

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

当然享受便当的一起,也要付出责任,LoginActivity 也需求完结 HasAndroidInjector,并声明 androidInjector,向下为它的 fragment 们供给注入。

3.3 dagger.android 的问题

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

  • 经过 @ContributesAndroidInjector 生成 subcomponent 及其 factory,省去了咱们显现地界说子组件,父组件也不需求再声明 Subcomponent.Factory
  • 让 Android 各层级方针持有 AndroidInjector,经过静态办法完结对低层级方针的注入,省去了显现地创立子组件完结注入

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

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

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

相对于 dagger.android 帮咱们生成 LoginActivitySubComponent, Hilt 中索性不允许自界说的 subcomponent,供给了预界说的 ActivityComponent 作为一切 activity 同享的供给注入的组件。而 LoginActivityModule 等原本装置到 LoginActivitySubComponent 的依靠,经过 @installIn 注解装置到 ActivityComponent 中。

ActivityComponent 是个 interface,编译期生成完结类 ActivityC

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

modules 中能够看到各 activity 依靠的 XXXActivityModule 都被 instllInActivityC 中,ActivityC 能够为一切 activity 供给注入。

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

FragmentCBuilderModuleViewCBuilderModule 用来装置 Hilt 别的两个预界说组件 FragmentComponentViewComponent,Hilt 为 Android 中的要害概念都供给了对应的预界说组件,且将它们树立树行联系。

4.2 预界说 Inject

dagger.android 经过供给静态办法注入下降了 inject 的本钱。而在 Hilt 中,inject 的本钱趋近于零,只需求在 activity 等 Android 组件增加 @AndroidEntryPoint 注解,其他什么都不用做。

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

前面看到 ActivityC 完结了 XXXActivity_GeneratedInjector 接口,这些接口便是 @AndroidEntryPoint 的产品

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

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

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

而 Hilt 经过 Transform + ASM,让编译后 LoginActivity 承继了 Hilt_LoginActivity,这样就能够在不写任何代码的情况下,让 LoginActivity 基于 Hilt 的 ActivityC 完结注入,即所谓的 “预界说 inject”。

4.3 预界说 @Scope

Dagger 在 Android 中运用时,往往需求需求自界说效果域注解标明不同 Android 类的生命周期。Hilt 伴随着预界说组件,也供给了与之对应的预界说效果域注解

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

例如,增加了 @ActivityScoped 注解,表示 provides 的方针在 ActivityComponent 规模内以单例存在。

Hilt 为一切的要害的 Android 类都供给了预界说组件和相对应的预界说效果域注解,所以也能够说 Hilt 对整棵组件树进行了预界说:

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

5. 渐进式迁移

经过前面介绍,咱们能感遭到 Hilt 对 Dagger 的 Boilerplate 问题进行了比较完全的改善,主张咱们赶快晋级到 Hilt。从组件树的视角来了解 Dagger 与 Hilt 的差异,能够帮助咱们完结渐进式的晋级。

安全的晋级过程便是从沿着组件树的树干,依照 Application -> Activity -> Fragment -> ... 的顺序,将 Dagger 的自界说组件兼并到 Hilt 的预界说组件,最终完结依靠注入完全托管到 Hilt 树。

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

5.1 将 Dagger 组件兼并到 Hilt

咱们以 ApplicationComponent 为例看一下,看一下 Dagger 中自界说 ApplicationComponent 怎么兼并到 Hilt 的预界说的 SingletonComponent

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

兼并后的代码如下所示,中心是 @EntryPoint 的运用:

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

  • @EntryPointSingletonC 完结了 ApplicationComponent 接口,代码中其他依靠 ApplicationComponent 的当地能够无缝切换到 SingletonComponent,两棵树在根节点完结兼并。
  • 新界说一个 module,经过 includes 将原本 ApplicationComponent 的依靠打包装置到 SingletonComponent

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

如上,在 App 中增加 @HiltAndroidEntryPoint 注解,Hilt 能够为 App 供给注入服务。可是 App 的 component 成员不能当即删去,或许还有其他代码在引用它。可是在根节点兼并后,咱们能够经过 EntryPointAccessors 从 Hilt 获取 ApplicationComponent 的完结。

其他层级的组件,例如 ActivityComponent, FragmemtComponent 等,也能够模仿 ApplicationComponent 向 Hilt 做兼并。如果你运用的是 dagger.android,则不再需求 @ContributesAndroidInjector 生成预界说组件了,能够删去相关代码,将依靠的 module 装置到 Hilt 的对应组件,如下:

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

5.2 整理 Dagger 残留代码

当咱们将组件树上的一切组件都兼并到 Hilt,Android 类都转向经过 Hilt 获取依靠注入,所以 Dagger 或许 dagger.android 相关的代码都能够整理掉了

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

如上,以 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 一定能满足你的需求,快用起来吧 ~

本文视频:www.bilibili.com/video/BV1cg…