![](/style/images/good.png)
![](/style/images/bad.png)
Dagger之十九、Hilt 迁移实战
source link: http://blog.chengyunfeng.com/?p=1131
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Dagger之十九、Hilt 迁移实战
本节课通过一个实际使用 Dagger 的项目来逐步的迁移到 Hilt 来看看如何迁移和使用 Hilt。 这个迁移教程是 Hilt 提供的一个官方的迁移示例项目,在前面课程中的示例项目也是使用了该项目,只不过该项目是使用 Kotlin 语言,而在前面的课程中把它修改为了大家熟悉的 Java 语言。
在本节迁移实战中,我们直接使用 Kotlin 语言, 如果你对 Kotlin 语言不太熟悉也没关系,有了前面课程的铺垫,即使不了解 Kotlin 语法也应该可以看懂每一个迁移步骤的。
在迁移到 Hilt 之前的项目代码位于 https://github.com/googlecodelabs/android-dagger-to-hilt,你可以克隆该代码或者直接下载该代码。
下载后可以在 Android Studio 中打开。
该项目的 Git 库中一共有三个分支:
– master
项目的初始状态,上面所克隆和下载的代码就是该分支的代码,里面使用了 Dagger。
– interop
迁移的中间状态,这个时候 Dagger 和 Hilt 同时存在
– solution
迁移完成后的状态,包含如何使用 ViewModels
、以及测试。点击这里可以下载最终的代码。
该示例项目提供了一个简单的注册、登录、设置流程,里面的类的依赖关系如下:
https://codelabs.developers.google.com/codelabs/android-dagger-to-hilt/img/8ecf1f9088eb2bb6.png
图片中的箭头代表对象之间的依赖关系,称之为 应用依赖图 — 应用中的所有的类以及这些类之间的依赖关系。
下图是该应用所使用 Dagger 构造的部件对象依赖图。对象上面的圆点代表该对象在它所在的部件中是只有一个对象的(也就是被限定在了管理它的那个部件里面了)。
https://codelabs.developers.google.com/codelabs/android-dagger-to-hilt/img/a1b8656d7fc17b7d.png
下面就来看看应该如何一步一步的迁移该项目。
做好迁移计划
对于简单的项目你可以一次性的迁移到 Hilt,但是在实际的项目中,往往比较复杂一次性迁移需要很多时间,所以一小步一小步的迁移并且每次迁移后保证项目还能正确运行才是比较重要的。
所以在本示例项目中,我们按照前面第十六课介绍的迁移步骤来一步一步的迁移。根据本项目的功能和使用流程。我们计划先迁移 Application
上的 AppComponent
部件,然后按照应用的使用流程再迁移注册界面、然后继续迁移登录界面、最后是主界面和设置界面。
在迁移之前先来看看目前项目中的部件层级关系。项目的父部件是 AppComponent
,在该部件中包含了两个模块 StorageModule
和 AppSubcomponents
。 AppSubcomponents
是一个聚合模块接口,里面包含了另外三个子部件,该模块存在的意义就是把这三个子部件安装到 AppComponent
这个父部件上,在三个子部件分别是 RegistrationComponent
、LoginComponent
和 UserComponent
,每个部件的功能如下:
– LoginComponent 用来负责注入 LoginActivity
中的 @Inject
变量
– RegistrationComponent 用来负责注入 RegistrationActivity
、 EnterDetailsFragment
、TermsAndConditionsFragment
三个类里面的 @Inject
变量。该部件使用了自定义的 @ActivityScope
和 RegistrationActivity
实例绑定到了一起,意味着在 RegistrationActivity
实例中该部件中的 ActivityScope 对象只有一个实例。
– UserComponent 用来负责注入 MainActivity
和 SettingsActivity
中的 @Inject
变量
现在我们对该应用的 Dagger 部件有了大概的了解,下面可以开始动手迁移了。
迁移 Application 部件
首先来迁移应用里面的 AppComponent
部件,在迁移的过程中需要做一些额外的工作来保证其他 Dagger 代码可以正常工作。
首先打开 MyApplication.kt
这个文件,在里面定义了 MyApplication
这个继承自 Application
的类,在该类上添加 @HiltAndroidApp
注解,这样就为该应用启用了 Hilt 。
MyApplication.kt
如果你的 Kotlin 语法不太了解的话, 上面的
return DaggerAppComponent.factory().create(applicationContext)
这行代码中的applicationContext
等价于调用getApplicationContext()
这个函数,返回值是一个 Application Context 对象。
迁移部件上的模块
打开 AppComponent.kt
文件可以发现 AppComponent
部件中定义了两个模块 StorageModule
和 AppSubcomponents
。现在开始迁移这两个模块,把他们安装到 Hilt 中的 ApplicationComponent
部件中。
打开 AppSubcomponents.kt
文件,并使用 @InstallIn(ApplicationComponent::class)
注解
ApplicationComponent::class
是 Kotlin 语言中用来引用 Java class 的语法。
对于 StorageModule
做同样的操作,打开 StorageModule.kt
文件:
现在在回去看一下 AppComponent.kt
文件中定义的 AppComponent
模块,该模块为 RegistrationComponent
、LoginComponent
和 UserManager
提供依赖对象,下一步我们就准备迁移这些子部件。
迁移对外暴露的类型
对于一个部件中的 对象提供函数 在 Hilt 中可以通过 Entry Points 来实现该功能。在这个步骤中,我们就使用 Entry Points 来从 Hilt 生成的 ApplicationComponent
中获取需要的对象。
在 RegistrationActivity
类中需要使用 RegistrationComponent.Factory
来创建 RegistrationComponent
子部件对象,然后 RegistrationActivity
使用该子部件对象来注入自己。 在迁移的时候,我们可以在 RegistrationActivity
类中定义个获取 RegistrationComponent.Factory
对象的 Entry Poinits,由于 RegistrationComponent.Factory
这个对象是从 ApplicationComponent
中获取的,所以需要把该 Entry Points 安装到 ApplicationComponent
中,定义方式如下:
RegistrationActivity.kt
然后把 Activity 中获取 RegistrationComponent.Factory
对象的代码修改为使用 RegistrationEntryPoint
来获取子部件:
RegistrationActivity.kt
通过上面的修改,现在在 Activity 中可以从 Hilt 生成的部件中获取 Dagger 子部件了。通过 Entry Points 把 Dagger 代码和 Hilt 代码连接到了一起。
现在按照上面同样的方式给其他 AppComponent
对外暴露的对象添加一个 Entry Points接口。
继续修改使用 LoginComponent.Factory
的地方,打开 LoginActivity.kt
文件并创建一个 Entry Points 接口:
LoginActivity.kt
继续修改 LoginActivity.kt
中获取 LoginComponent
对象并实现注入的代码:
现在 AppComponent
对外暴露的三个类型中,有两个已经替换成了 Hilt 所支持的 Entry Points 实现。现在就剩 UserManager
这个类型了,这个类型和 RegistrationComponent
、LoginComponent
比起来有点不一样,该类被 MainActivity
和 SettingsActivity
这两个类同时使用。我们不需要在每个 Activity 中都创建一个 Entry Points,只需要创建一个就可以了。我们可以先把该接口定义到 MainActivity
中:
打开 MainActivity.kt
文件来创建这个 Entry Points:
然后在 MainActivity.kt
和 SettingsActivity.kt
中分别修改使用 UserManager
的代码:
MainActivity.kt
SettingsActivity.kt
Entry Points 通常创建在使用他们的地方。如果一个 Entry Points 被用于多个地方,也可以选择把 Entry Points 定义到一个新的类中,并把该类放到一个类似于
util
这种 Java 包下。
删除部件工厂类定义
在部件工厂类中还使用了 @BindsInstance
来把 Context
对象添加到了部件中,这说明当前有依赖对象需要使用 Context
对象,经过分析我们发现 SharedPreferencesStorage
类的构造函数需要 Context
对象,在 Hilt 中默认已经在部件中支持了 Context
对象,现在修改 SharedPreferencesStorage
函数来使用 Hilt 所提供的 Context
对象:
SharedPreferencesStorage.kt
检测部件中的对象注入函数
现在看看部件中定义了哪些对象注入函数(inject()
函数),然后在需要注入的类上使用 @AndroidEntryPoint
注解。在示例项目中的 AppComponent
部件中并没有定义 inject()
函数,所以这一步我们啥都不需要做。
删除 AppComponent 类
前面已经为 AppComponent
类中的所有所提供的对象都创建了 EntryPoints
接口,现在可以删除 AppComponent.kt
了。
删除使用 AppComponent
的代码
现在在 Application
类中不再需要初始化 AppComponent
部件对象了,可以删除 Application
类中的相关代码了,删除后该类的代码如下:
MyApplication.kt
现在已经成功的把 Hilt 应用到了项目中的 Application
上,之前定义的 AppComponent
部件已经完全被 Hilt 生成的 ApplicationComponent
替代了。
现在编译并运行该应用,该应用应该可以继续正确的运行。下一步继续迁移 Activity 和 Fragment。
迁移 Activity 部件
先来迁移登录流程,当前 LoginActivity
是通过 Entry Point 来获取 LoginComponent
的,需要在 Activity 中调用我们自己编写的代码来实现对 Activity 的注入,我们想让 Hilt 来自动实现该功能。
打开 LoginComponent.kt
文件发现 LoginComponent
并没有包含任何 Dagger 模块,所以这里针对模块不需要做任何事情,只需要在 LoginActivity
类上添加 @AndroidEntryPoint
即可:
这样 Hilt 就可以管理 LoginActivity
类了,我们之前定义的 LoginEntryPoint
和使用该接口的代码都可以删除了。
LoginActivity.kt
最后删除 LoginComponent.kt
文件,同时从 AppSubcomponents.kt
中删除对 LoginComponent
的引用:
上面就是把 LoginActivity
迁移到 Hilt 所需要做的改动。上面的改动主要是删除了大量的代码,然后 Hilt 自动生成我们需要的东西。下面继续迁移 Fragment 。
迁移 Fragment 部件
这一步来迁移注册流程。在迁移之前先来看看 RegistrationComponent
部件,打开 RegistrationComponent.kt
文件找到里面所定义的 inject()
函数(成员注入函数),可以看到该部件中定义了三个该函数,这意味着该部件负责注入 RegistrationActivity
、EnterDetailsFragment
和 TermsAndConditionsFragment
这三个类中的 @Inject
变量。
先从 RegistrationActivity
类开始,打开 RegistrationActivity.kt
文件给该类添加 @AndroidEntryPoint
注解:
RegistrationActivity.kt
这样该类就交给 Hilt 来管理了,然后就可以和 LoginActivity
一样删除 RegistrationEntryPoint
接口以及使用该 Entry Points 的相关代码了。
RegistrationActivity.kt
现在该类中的需要注入的变量由 Hilt 自动完成注入,所以不需要该类中的 registrationComponent
变量了,也不需要通过手工的调用该 registrationComponent
子部件来注入这个类了。
RegistrationActivity.kt
修改完了 Activity ,继续打开 EnterDetailsFragment.kt
这个文件,在 EnterDetailsFragment
类上面使用 @AndroidEntryPoint
注解。
EnterDetailsFragment.kt
这样该 Fragment 中需要注入的变量也由 Hilt 来注入,所以该 Fragment 里面的 onAttach()
可以删除了。
然后继续迁移 TermsAndConditionsFragment
类,打开 TermsAndConditionsFragment.kt
文件,在该类上使用 @AndroidEntryPoint
注解并删除 onAttach()
函数:
TermsAndConditionsFragment.kt
现在 RegistrationComponent
中需要注入的 Activity 和 Fragment 都迁移到 Hilt 了,所以可以删除 RegistrationComponent.kt
文件了,同时从 AppSubcomponents
中删除对该子部件的引用:
AppSubcomponents.kt
现在还差最后一步就完成了对注册流程的迁移。这最后一步就是自定义范围(Scope),在注册部件上使用了自定义的 ActivityScope
,自定义范围是用来控制部件中依赖对象的生命周期的。在这个示例中,ActivityScope
告诉 Dagger 在 RegistrationActivity
这个 Activity 的生命周期内,注入的 RegistrationViewModel
对象都是同一个实例。 Hilt 已经预定义了对应的自定义范围了,这里我们可以使用 Hilt 预定义的 @ActivityScoped
,打开 RegistrationViewModel
文件,把上面的 @ActivityScope
注解替换为 @ActivityScoped
:
RegistrationViewModel.kt
上面的
@Inject constructor
是 Kotlin 定义构造函数的语法,相当于在 Java 里面的构造函数上使用了@Inject
注解。
现在自定义的 ActivityScope
没有人使用了,可以安全的删除这个类了。
现在编译应用并运行,你可以继续使用之前已经注册的用户名和密码来登录,也可以注销账号从新注册一个,应用行为表现正常。
现在在该应用中同时在使用 Hilt 和 Dagger ,除了 UserManager
这个依赖对象之外,其他的对象都是 Hilt 来注入和管理的。下一步来看看这个 UserManager
如何迁移。
如果这一步你的代码有问题,则可以下载正确的代码 来和你的代码做下对比,看看哪一步没有迁移正确。
迁移另一个范围部件
现在就剩 UserComponent
这最后一个部件还没迁移到 Hilt 了。该部件使用了一个自定义的 @LoggedUserScope
范围注解。这意味着,对于使用了 @LoggedUserScope
注解的类 UserComponent
部件会使用同一个 UserManager
对象来注入这些类。
UserComponent
没有对应的 Hilt 部件,该部件的生命周期和安卓组件的生命周期不一致。由于Hilt不支持把自定义部件添加到 Hilt 所生成的部件层级中
,所以需要对该子部件功能做下调整,你可以有两个选择:
- 同时使用 Dagger 子部件和 Hilt,就像目前的代码一样,让他们同时存在一个项目中。
- 把该自定义范围子部件中的对象迁移到最接近该子部件的 Hilt 部件中(这该示例项目中是
ApplicationComponent
部件),当需要的时候可以设置该部件中依赖对象为 null 值。
现在来看看如何实现第二个选项,把该项目完全迁移到 Hilt。
我们选择把 UserComponent
迁移到 Hilt 生成的 ApplicationComponent
中,所以该部件中的模块需要安装到 ApplicationComponent
中。
UserComponent
模块中只有一个使用了自定义范围 LoggedUserScope
注解的范围对象 — UserDataRepository
。由于我们把该子部件合并到了 ApplicationComponent
中,所以 UserDataRepository
上面的范围注解需要替换为 @Singleton
,并且修改代码逻辑当用户没有登录的时候让该对象的值为 null。
UserManager
已经使用了 @Singleton
,说明在整个应用生命周期中,获得到的 UserManager
对象都是一样的,对代码做些修改,可以实现前面同样的功能。在动手修改之前,先要对 UserManager
和 UserDataRepository
的功能做下调整。
打开 UserManager.kt
文件按照如下步骤修改该文件:
- 由于不需要
UserComponent
子部件了,把构造函数中的UserComponent.Factory
参数修改为UserDataRepository
。这样UserManager
依赖UserDataRepository
对象。 - 由于 Hilt 会自动生成需要的部件,可以删除
UserComponent
相关的代码了 - 修改
isUserLoggedIn()
函数,之前是检查userComponent
是否为 null 来判断用户是否登录,现在修改为判断userRepository
中的 用户名 是否为 null 来确定用户是否登录。 - 在
userJustLoggedIn()
函数中添加一个用户名作为参数 - 修改
userJustLoggedIn()
函数,使用用户名参数来调用userDataRepository#initData()
函数 - 在
registerUser()
和loginUser()
函数中调用userJustLoggedIn()
函数,参数为登录的用户名 - 删除
logout()
函数中的userComponent
代码,替换为调用userDataRepository.cleanUp()
函数
修改完以后的 UserManager.kt
代码如下:
修改完 UserManager
后还需要再来修改下 UserDataRepository
类,打开 UserDataRepository.kt
文件,做如下修改:
- 现在依赖对象都通过 Hilt 来管理,可以删除
LoggedUserScope
注解了 - 在
UserManager
中已经需要依赖UserDataRepository
对象了,为了避免循环依赖,可以删除UserDataRepository
构造函数上的UserManager
参数了 - 把
unreadNotifications
变量设置为可以为 null 值,并且把 setter 函数设置为private
的 - 添加一个新的变量
username
,该变量可以为 null 值,并且把 setter 函数设置为private
的 - 添加一个
initData()
函数,该函数设置username
和unreadNotifications
这两个变量 - 添加一个
cleanUp()
函数,该函数重置username
和unreadNotifications
这两个变量的值 - 最后把
randomInt()
函数移动到类文件中
修改完后,UserDataRepository.kt
代码看起来是这个样子的:
上面的修改相当于修改了如何判断用户是否登录的逻辑,现在不是通过 UserDataRepository
对象是否为 null 来判断了,而是通过用户名是否为 null 来判断,这样 Hilt 就可以注入 UserDataRepository
对象了,而不用像前面一样,当用户登录了才去注入。
现在可以开始迁移 UserComponent.kt
了,打开该类文件,找到 inject()
函数,发现该部件在 MainActivity
和 SettingsActivity
中使用。先来迁移 MainActivity
,打开该类在上面添加 @AndroidEntryPoint
注解:
MainActivity.kt
删除里面的 UserManagerEntryPoint
接口和相关的代码。
MainActivity.kt
然后在该类中添加一个需要注入的 UserManager
变量:
MainActivity.kt
现在这个 UserManager
对象通过 Hilt 来注入,可以删除调用 UserComponent#inject()
这个函数的代码了
MainActivity.kt
上面是 MainActivity
中需要做的修改,现在继续修改 SettingsActivity
类,先在上面添加 @AndroidEntryPoint
并同样添加一个 UserManager
对象:
然后删除调用 userComponent
相关的代码,修改完以后 SettingsActivity
的 onCreate
函数应该是这样的:
SettingsActivity.kt
现在可以删除 LoggedUserScope.kt、UserComponent.kt 和 AppSubcomponent.kt
这些 Dagger 中所定义的代码了。
现在整个应用全部迁移到 Hilt 了,编译安装该应用可以看到起功能和之前使用 Dagger 时候的情况是一样的,迁移是成功的。
现在应用代码已经迁移到 Hilt 了,就差测试代码了。 先来看看单元测试如何使用 Hilt。
在测试中你也可以选择不用 Hilt 来注入对象,可以直接手工的使用构造函数来创建对象。比如我们的 UserManagerTest
单元测试就是这样的,目前的 UserManagerTest
单元测试无法执行,原因在于我们对 UserManager
的逻辑做了修改,所以需要修改一下相关的测试代码:
UserManagerTest.kt
修改后再运行代码,该测试应该就可以通过了。
添加测试依赖对象
在进入 Hilt 测试之前,先打开 应用的 app/build.gradle
配置文件,在里面添加 Hilt 测试库 :
由于在测试中,Hilt 也要用来生成需要的类,所以需要添加 kaptAndroidTest
Hilt 会自动的为每个测试案例生成测试的部件和测试的 Application。打开 TestAppComponent.kt
文件来开始迁移到 Hilt。TestAppComponent
有两个模块 TestStorageModule
和 AppSubcomponents
,在前面的迁移中已经删除了 AppSubcomponents
类,现在可以继续迁移 TestStorageModule
模块。
打开 TestStorageModule.kt
类,在该类上使用 @InstallIn(ApplicationComponent::class)
注解:
TestStorageModule.kt
现在所有的测试模块也迁移完毕了,可以去删除 TestAppComponent
类了。
下面可以把 Hilt 添加到 ApplicationTest
中了,先把所有使用 Hilt 的 UI 测试 修改为使用 @HiltAndroidTest
注解。该注解告诉 Hilt 为每个测试生成 Hilt 部件。
打开 ApplicationTest.kt
文件添加如下的注解:
– @HiltAndroidTest
告诉 Hilt 为测试生成需要的部件
– @UninstallModules(StorageModule::class)
告诉 Hilt 把 StorageModule
从依赖部件中移除,在测试的过程中使用 TestStorageModule
模块替代
– 在 ApplicationTest
中添加 HiltAndroidRule
,该 Rule 用来管理部件的状态,还用来注入测试需要的对象。最终修改后的代码应该是这样的:
ApplicationTest.kt
当执行测试的时候,还需要告诉测试框架使用 Hilt 所生成的 Application
,通过自定义 test runner 来实现这个需求。在该示例项目中已经定义了一个自定义的 Test runner,打开 MyCustomTestRunner.kt
文件。在 test runner 的 newApplication
函数中使用 Hilt 提供的测试 Application 即可:
MyCustomTestRunner.kt
修改完毕后可以删除之前自定义的 MyTestApplication.kt
文件了。然后运行下测试,所有的测试应该都可以通过。
使用 Jetpack 中的 ViewModel
Hilt 目前还支持 Jetpack 库中的 WorkManager 和 ViewModel。在该示例项目中,我们的 ViewModel 并没有继承自 Jetpack 中的 ViewModel 类。为了演示 Hilt 对ViewModel 的支持,我们可以把项目中的 MainViewModel
修改为继承自 Jetpack 中的 VIewModel。
在使用 ViewModel 之前需要现在 app/build.gradle
配置文件中添加需要的库:
然后打开 MainViewModel.kt
类,让该类继承自 Jetpack 中的 ViewModel。然后在该类的构造函数上使用 @ViewModelInject
注解这样 Hilt 就知道如何创建这个 ViewModel 对象了。
MainViewModel.kt
对于 LoginViewModel
类做同样的修改
LoginViewModel.kt
对于 RegistrationViewModel
类也是如此,通过扩展函数 viewModels() 和 activityViewModels()
已经可以控制 ViewModel 的生命周期了所以在 RegistrationViewModel
中不需要 @ActivityScoped
注解了。
RegistrationViewModel.kt
继续修改 EnterDetailsViewModel
和 SettingViewModel
EnterDetailsViewModel.kt
SettingViewModel.kt
现在所有的 ViewModel 类都迁移到 Jetpack ViewModel 了。可以开始迁移他们被注入的代码了。
在 Jetpack 中,ViewModel 是通过 by viewModels()
代理函数来获取到的。
打开 MainActivity.kt
来修改定义 ViewModel
的变量 mainViewModel
,现在由于该变量是通过代理函数赋值的,所以可以设置为 private 的:
by viewModels() 是 Kotlin 提供的工具扩展函数,类似于 Java 里面助手类,可以用来获取该 Activity 中的ViewModel 对象
MainActivity.kt
对于其他几个 Activity 也需要同样的修改:
LoginActivity.kt
RegistrationActivity.kt
RegistrationActivity.kt
EnterDetailsFragment.kt
SettingsActivity.kt
由于下面两个 Fragment 之前使用的是 Activity 范围的对象,所以这里通过 activityViewModels()
代理函数来注入 Activity 里面的 ViewModel 对象,相当于 这两个 Fragemnt 中所使用的 ViewModel 对象的依赖对象是 Activity 中共享的对象。
EnterDetailsFragment.kt
TermsAndConditionsFragment.kt
这样就把所有的 ViewModel 都迁移到 Jetpack ViewModel 了,并且使用 Hilt 来创建每个 ViewModel。
通过一步一步的 把 Dagger 项目迁移到 Hilt,可以看出在迁移 Dagger 项目的时候,还是需要详细规划每步应该迁移那个组件,特别是哪些和安卓组件没有对应关系的自定义组件的迁移,遇到这种情况,需要修改一下业务逻辑保证能够把这些组件提供的功能放到对应的安卓组件中,这样才能完全迁移到 Hilt 框架中,如果你之前的代码业务很复杂,短期内无法迁移这些子部件,那么就只能让 Hilt 和 Dagger子部件共存了。
而对于刚刚开始使用 Hilt 的新项目则不存在这个问题,并且 Hilt 在使用的时候更具规范和简单。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK