首页 > 其他分享 >从 Component Tree 视角看 Dagger 到 Hilt 的演变

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

时间:2023-07-29 20:04:49浏览次数:43  
标签:dagger Tree Component Hilt activity 组件 Dagger android

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 的演变_Android Jetpack

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

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

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

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

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

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

2. 使用 @Subcomponent 构建组件树

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

2.1 定义子组件

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

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

2.2 建立组件父子关系

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

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

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

2.3 使用子组件注入

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

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

如上,在合适的时机,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 的演变_bc_06

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

3.1 @ContributesAndroidInjector 生成子组件

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

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

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

3.2 DispatchingAndroidInjector 提供组件映射

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

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

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

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

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

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

如上,在 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 的演变_Android_11

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_12

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

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

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

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

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

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

4.2 预定义 Inject

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

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

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

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

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 的演变_android_17

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

4.3 预定义 @Scope

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

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

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

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

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

5. 渐进式迁移

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

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

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

5.1 将 Dagger 组件合并到 Hilt

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

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

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

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

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

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

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

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

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

5.2 清理 Dagger 残留代码

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

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

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



标签:dagger,Tree,Component,Hilt,activity,组件,Dagger,android
From: https://blog.51cto.com/u_15200109/6895035

相关文章

  • [odoo开发笔记05]odoo 15&16 Tree/看板视图添加按钮
    odoo在15及之后版本产生js引用变更,导致14及之前列表视图(Tree/List)添加自定义按钮的方式产生了变化。目前15/16版本列表视图添加按钮有三种方式1.每个明细行上都显示按钮此种Tree视图添加按钮仅需要定位第一个字段,添加button即可创建xml文件(例如sale_view.xml)写入以下内容<?......
  • Web Component 简单示例
    前言学习内容来源:https://www.youtube.com/watch?v=2I7uX8m0Ta0https://developer.mozilla.org/zh-CN/docs/Web/API/Web_components基本概念Customelement(自定义元素):class或者function,定义组件apiShadowDOM(影子DOM):用于将封装的“影子”DOM树附加到元素(与主文档DOM......
  • Ztree树的直接显示
     首先在你要想显示的地方加入以下代码:<divclass="layui-col-md3"style="width:21%;height:100vh"><divid="treeDemo"class="ztree"></div></div>然后引入(位置不一定一样请根据自己的情况做调整);<linkhref="......
  • zTree树的创建、多选、多选框信息回显附带zTree树官网地址
    zTree树官网:https://www.treejs.cn/v3/main.php#_zTreeInfo 首先来到首页点击右上角下载 可以选择使用Git拉取也可以下载ZIP压缩包,两者选其一即可下载打开文件夹以后是下面这些文件然后将这些文件放你所使用的入项目当中,如下图 将文件放入......
  • .Net6基于layui和ztree完成树形选择器添加和反填和修改
    以责任科室为例存储两个值ResponsibleDepartment和AoId,ResponsibleDepartment:是科室名称,AoId是科室Id添加:<divclass="layui-form-itemlayui-form-text"><labelclass="layui-form-label">责任科室</label><divclass="layui-inpu......
  • 决策树插件——TreePlan Excel plugin学习指导
    决策树是一种常用的风险型决策工具,它能够帮助人们在不确定性和风险环境下做出合理的决策。决策树通过模拟决策过程,以树状结构呈现不同决策路径和结果,并通过计算风险与收益之间的权衡来选择最优的决策方案。决策树的优势在于它能够清晰地展示各种决策选项以及每个选项下可能的结果......
  • ztree分类页面代码
    ztree分类页面代码    后台代码:[Area("Adnn1n")]publicclassCategoryController:BaseController{privatereadonlyDAL.Interface.ICategorydal;publicCategoryController(ICategorydal){this.dal=......
  • zTree -- jQuery 树插件的使用包括添加、编辑(MVC)
    zTree--jQuery树插件网址:https://www.treejs.cn/v3/main.php#_zTreeInfo自行下载所需要的文件我自己写的一些具体示例:使用的.netCore6后端使用的ORM框架SqlSugar的中的ToTreeAsync方法返回的需要的数据格式,如果没用SqlSugar可以自己写递归来完成publicasyncTask<Resul......
  • @mapper(componentModel = “spring”)
    在接口上使用该注解,可以自动生成该接口的实现类.实现DTO-DO各种模型之间的字段映射(不仅仅限制于DTO-DO)https://blog.csdn.net/qq_36937844/article/details/126848404......
  • 递归实现对TreeView的Node的填充
    树的数据结构是从根节点开枝散叶,父节点唯一。首先初始化要展示的数据,用Dictionary保存:Dictionary<int,List<int>>dt;初始化数据,数字0为根节点,字典中的key有[0,1,2,3,4,11,12,13,14,21,22,23,24,31,32,33,34,41,42,43,44]:privatevoidInitData(){dt=newDiction......