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

6 篇文章 1 订阅
2 篇文章 0 订阅

本文视频: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 成员。

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

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

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

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

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

2. 使用 @Subcomponent 构建组件树

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

2.1 定义子组件

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

2.2 建立组件父子关系

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

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

2.3 使用子组件注入

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

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

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

3.1 @ContributesAndroidInjector 生成子组件

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

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

3.2 DispatchingAndroidInjector 提供组件映射

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

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

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

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

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

3.4 dagger.android 的问题

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

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

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

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

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

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

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

4.2 预定义 Inject

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

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

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

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

4.3 预定义 @Scope

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

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

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

5. 渐进式迁移

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

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

5.1 将 Dagger 组件合并到 Hilt

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

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

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

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

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

5.2 清理 Dagger 残留代码

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

如上,以 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/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Hilt 是一个用于简化依赖注入的框架,可以在 Android 应用程序中使用它来管理和注入依赖项。下面是使用 Hilt 进行依赖注入的一个简单示例应用: 1. 添加 Hilt 相关依赖到项目中。在项目的根目录的 `build.gradle` 文件中添加 Hilt Gradle 插件的依赖: ```groovy buildscript { repositories { ... } dependencies { ... classpath 'com.google.dagger:hilt-android-gradle-plugin:<version>' } } ``` 在应用模块的 `build.gradle` 文件中应用 Hilt 插件,并添加 Hilt 相关依赖: ```groovy apply plugin: 'kotlin-kapt' apply plugin: 'dagger.hilt.android.plugin' android { ... } dependencies { implementation 'com.google.dagger:hilt-android:<version>' kapt 'com.google.dagger:hilt-android-compiler:<version>' ... } ``` 2. 在 Application 类上添加 `@HiltAndroidApp` 注解,将其标记为 Hilt 应用程序: ```kotlin import dagger.hilt.android.HiltAndroidApp @HiltAndroidApp class MyApp : Application() { // Application 的实现代码... } ``` 3. 创建一个需要依赖注入的类,例如 UserRepository: ```kotlin import javax.inject.Inject class UserRepository @Inject constructor() { // UserRepository 的实现代码... } ``` 4. 在需要进行依赖注入的类中,使用 `@Inject` 注解来标记需要注入的依赖项。例如,在 MainActivity 中: ```kotlin import javax.inject.Inject @AndroidEntryPoint class MainActivity : AppCompatActivity() { @Inject lateinit var userRepository: UserRepository // Activity 的实现代码... } ``` 通过以上步骤,Hilt 会自动管理依赖项的创建和注入。在 MainActivity 中,`userRepository` 字段将会被自动注入一个 UserRepository 实例。 需要注意的是,使用 Hilt 进行依赖注入需要在 Android Studio 中启用 kapt 插件。 这只是一个简单的示例,Hilt 可以在更复杂的应用程序中提供更多的依赖注入功能和灵活性。 希望对你有所帮助!如果还有其他问题,请随时提问。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

fundroid

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值