Thông tin về lớp học lập trình này
1. 简介
在本 Codelab 中,您将学习如何将 Dagger 组件迁移到 Hilt,以便在 Android 应用中实现依赖项注入 (DI)。之前的 Codelab 描述了如何“在 Android 应用中使用 Dagger”,而本文将教您如何从 Dagger 迁移到 Hilt。本 Codelab 旨在介绍如何规划迁移,并且在将每个 Dagger 组件迁移到 Hilt 的过程中保持应用正常运行,从而保证 Dagger 和 Hilt 在迁移期间能够并行工作。
依赖项注入有助于提高代码的可重用性,便于进行重构和测试。Hilt 基于热门 DI 库 Dagger 构建,因而能够受益于 Dagger 的编译时正确、高运行时性能、可伸缩性以及支持 Android Studio 等特点。
由于许多 Android 框架类是由操作系统本身实例化的,因此在 Android 应用中使用 Dagger 时会有关联的样板代码。Hilt 可自动生成和提供以下内容,从而可以省去此样板代码中的大部分内容:
- 用于将 Android 框架类与 Dagger 集成的组件 - 您不必手动创建。
- 组件的作用域注解 - 由 Hilt 自动生成。
- 预定义的绑定和限定符。
最重要的是,由于 Dagger 和 Hilt 可以共存,因此您可以根据需要迁移应用。
如果您在学习本 Codelab 时遇到任何问题(代码错误、语法错误、内容含义不清等),请通过 Codelab 左下角的“报告错误”链接报告该问题。
前提条件
- 有使用 Kotlin 语法的经验。
- 有使用 Dagger 的经验。
学习内容
- 如何将 Hilt 添加到 Android 应用。
- 如何规划迁移策略。
- 如何将组件迁移到 Hilt 并保证现有的 Dagger 代码正常运行。
- 如何迁移限定了作用域的组件。
- 如何使用 Hilt 测试应用。
所需条件
- Android Studio 4.0 或更高版本。
2. 准备工作
获取代码
从 GitHub 获取 Codelab 代码:
$ git clone https://github.com/googlecodelabs/android-dagger-to-hilt
或者,您可以下载代码库的 Zip 文件:
打开 Android Studio
如果您需要下载 Android Studio,可以在此处下载。
项目设置
本项目使用了多个 GitHub 分支进行构建:
master是您签出或下载的分支,也是本 Codelab 的起点。interop是 Dagger 和 Hilt 互操作分支。solution中包含了本 Codelab 的解决方案,包括测试和 ViewModel。
建议您从 master 分支开始,按照自己的节奏逐步完成本 Codelab。
在本 Codelab 中,系统会向您提供需要添加到项目中的代码段。在某些地方,您还必须移除代码,我们将在代码段的注释中明确标出这些代码。
作为检查点,如果在特定步骤中需要帮助,您可以使用中间分支。
如需使用 Git 获取 solution 分支,请使用以下命令:
$ git clone -b solution https://github.com/googlecodelabs/android-dagger-to-hilt
或从此处下载解决方案代码:
常见问题解答
运行示例应用
首先,我们来看看起始示例应用是什么样的。按照下列说明在 Android Studio 中打开示例应用。
- 如果您下载了 zip 归档文件,请在本地解压缩该文件。
- 在 Android Studio 中打开项目。
- 点击
运行按钮,然后选择模拟器或连接 Android 设备。此时会显示注册屏幕。

该应用包含 4 个使用 Dagger 的不同流程(作为 activity 实现):
- 注册:用户可以输入用户名和密码,并接受我们的条款及条件,从而完成注册。
- 登录:用户可以使用在注册流程中添加的凭据进行登录,也可以从应用中注销。
- 主屏幕:欢迎屏幕,用户可以查看有多少条未读通知。
- 设置:用户可以退出并刷新未读通知的数量(这将生成随机数量的通知)。
项目遵循典型的 MVVM 模式,将视图的所有复杂性都推延到 ViewModel 中。请花点时间熟悉一下项目的结构。

箭头表示对象之间的依赖关系。这就是我们所说的应用图:包含应用的所有类以及各个类之间的依赖关系。
master 分支中的代码使用 Dagger 注入依赖项。我们将重构应用,以使用 Hilt 来生成组件和其他与 Dagger 相关的代码,而不手动创建组件。
Dagger 在应用中的设置如下图所示。某些类型上的点表示该类型的作用域限定为提供它的组件:

3. 将 Hilt 添加到项目中
为简单起见,在您最初下载的 master 分支中,我们已将 Hilt 依赖项添加到了项目里。您无需将以下代码添加到项目中,因为此过程已完成。尽管如此,我们还是来看看在 Android 应用中使用 Hilt 需要做些什么。
除了库依赖项之外,Hilt 还会使用在项目中配置的 Gradle 插件。打开根(项目级)build.gradle 文件,并在类路径中找到以下 Hilt 依赖项:
buildscript {
...
ext.hilt_version = '2.28-alpha'
dependencies {
...
classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
}
}
打开 app/build.gradle 并检查顶层的 Hilt Gradle 插件声明(位于 kotlin-kapt 插件下方)。
...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
android {
...
}
最后,Hilt 依赖项和注解处理器包含在项目的同一个 app/build.gradle 文件中:
...
dependencies {
implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
}
在您构建和同步项目时会下载包括 Hilt 在内的所有库。那么,我们一起来使用 Hilt 吧!
4. 计划迁移
您可能很想一次将所有内容都迁移到 Hilt,但在实际项目中,您需要在保证应用构建和运行正常的情况下,将内容逐步迁移到 Hilt。
迁移到 Hilt 时,您需要将工作分成多个步骤。推荐的方法是从迁移应用或 @Singleton 组件开始,然后再迁移 activity 和 fragment。
在本 Codelab 中,您需要先迁移 AppComponent,然后迁移应用的每个流程,迁移顺序是“注册”、“登录”、“主屏幕”和“设置”。
在迁移过程中,您将移除所有 @Component 和 @Subcomponent 接口,并使用 @InstallIn 注解所有模块。
迁移后,应使用 @AndroidEntryPoint,对所有 Application/Activity/Fragment/View/Service/BroadcastReceiver 类进行注解,并且还应移除所有代码实例化或传播组件。
为了规划迁移,让我们从 AppComponent.kt 开始了解组件的层次结构。
@Singleton
// Definition of a Dagger component that adds info from the different modules to the graph
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
// Factory to create instances of the AppComponent
@Component.Factory
interface Factory {
// With @BindsInstance, the Context passed in will be available in the graph
fun create(@BindsInstance context: Context): AppComponent
}
// Types that can be retrieved from the graph
fun registrationComponent(): RegistrationComponent.Factory
fun loginComponent(): LoginComponent.Factory
fun userManager(): UserManager
}
AppComponent 使用 @Component 进行注解,并且包括两个模块:StorageModule 和 AppSubcomponents。
AppSubcomponents 有三个组件:RegistrationComponent、LoginComponent 和 UserComponent。
- 将
LoginComponent注入LoginActivity - 将
RegistrationComponent注入RegistrationActivity、EnterDetailsFragment和TermsAndConditionsFragment。此外,此组件的作用域限定为RegistrationActivity。
将 UserComponent 注入到 MainActivity 和 SettingsActivity。
对 ApplicationComponent 的引用可以替换为应用中想要迁移的组件所对应的 Hilt 组件(链接到所有生成的组件)。
5. 迁移应用组件
在本节中,您将迁移 AppComponent。在通过以下步骤将每个组件迁移到 Hilt 时,为确保现有的 Dagger 代码能够正常运行,您需要做一些基础工作。
如需初始化 Hilt 并开始生成代码,您需要使用 Hilt 注解来注解 Application 类。
打开 MyApplication.kt 并将 @HiltAndroidApp 注解添加到该类。这些注解会指示 Hilt 触发生成代码,Dagger 将获取该代码并在其注解处理器中加以使用。
MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application() {
// Instance of the AppComponent that will be used by all the Activities in the project
val appComponent: AppComponent by lazy {
initializeComponent()
}
open fun initializeComponent(): AppComponent {
// Creates an instance of AppComponent using its Factory constructor
// We pass the applicationContext that will be used as Context in the graph
return DaggerAppComponent.factory().create(applicationContext)
}
}
1. 迁移组件模块
首先,打开 AppComponent.kt。AppComponent 包含两个已添加到 @Component 注解中的模块(StorageModule 和 AppSubcomponents)。您需要做的第一件事是迁移这两个模块,以便 Hilt 将其添加到生成的 ApplicationComponent 中。
为此,请打开 AppSubcomponents.kt,并使用 @InstallIn 注解来注解该类。@InstallIn 注解利用参数向适当的组件中添加模块。在本例中,当迁移应用级组件时,您应在 ApplicationComponent 中生成绑定。
AppSubcomponents.kt
// This module tells a Component which are its subcomponents
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
RegistrationComponent::class,
LoginComponent::class,
UserComponent::class
]
)
class AppSubcomponents
您需要在 StorageModule 中做出同样的更改。与上一步一样,打开 StorageModule.kt 并添加 @InstallIn 注解。
StorageModule.kt
// Tells Dagger this is a Dagger module
// Install this module in Hilt-generated ApplicationComponent
@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {
// Makes Dagger provide SharedPreferencesStorage when a Storage type is requested
@Binds
abstract fun provideStorage(storage: SharedPreferencesStorage): Storage
}
通过 @InstallIn 注解再次告知 Hilt 将模块添加到 Hilt 生成的 ApplicationComponent 中。
现在回头检查 AppComponent.kt。AppComponent 为 RegistrationComponent、LoginComponent 和 UserManager 提供依赖项。在接下来的步骤中,您需要准备这些组件以开展迁移工作。
2. 迁移公开类型
当您将应用完全迁移到 Hilt 时,Hilt 支持您使用入口点从 Dagger 手动请求获取依赖项。通过使用入口点,您可以在迁移每个 Dagger 组件的过程中让应用保持正常运行。在此步骤中,您将在 Hilt 生成的 ApplicationComponent 中通过手动查找依赖项来替换每个 Dagger 组件。
如需从 Hilt 生成的 ApplicationComponent 中获取 RegistrationActivity.kt 的 RegistrationComponent.Factory,您需要创建用 @InstallIn 注解的新 EntryPoint 接口。InstallIn 注解会指示 Hilt 从何处获取绑定。如需访问入口点,请使用 EntryPointAccessors 中相应的静态方法。参数应该是组件实例或充当组件持有者的 @AndroidEntryPoint 对象。
RegistrationActivity.kt
class RegistrationActivity : AppCompatActivity() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface RegistrationEntryPoint {
fun registrationComponent(): RegistrationComponent.Factory
}
...
}
现在,您需要用 RegistrationEntryPoint 替换与 Dagger 相关的代码。将 registrationComponent 的初始化更改为使用 RegistrationEntryPoint。进行这项更改后,RegistrationActivity 可以通过 Hilt 生成的代码访问其依赖项,直到将其迁移为使用 Hilt 为止。
RegistrationActivity.kt
// Creates an instance of Registration component by grabbing the factory from the app graph
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, RegistrationEntryPoint::class.java)
registrationComponent = entryPoint.registrationComponent().create()
接下来,您需要为所有其他公开类型的组件执行相同的基础工作。我们来继续处理 LoginComponent.Factory。像前面一样打开 LoginActivity 并创建一个用 @InstallIn 和 @EntryPoint 注解的 LoginEntryPoint 接口,但这次我们从 Hilt 组件公开 LoginActivity 的需求。
LoginActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface LoginEntryPoint {
fun loginComponent(): LoginComponent.Factory
}
现在 Hilt 已经知道如何提供 LoginComponent,请将旧的 inject() 调用替换为 EntryPoint 的 loginComponent()。
LoginActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
entryPoint.loginComponent().create().inject(this)
AppComponent 中三种公开类型的两种已替换,以与 Hilt EntryPoints 搭配使用。接下来,您需要对 UserManager 进行类似的更改。与 RegistrationComponent 和 LoginComponent 不同,UserManager 可同时用于 MainActivity 和 SettingsActivity。您只需要创建一次 EntryPoint 接口即可。注解的 EntryPoint 接口可用于这两个 activity。为简单起见,请在 MainActivity 中声明 Interface。
如需创建 UserManagerEntryPoint 接口,请打开 MainActivity.kt,并使用 @InstallIn 和 @EntryPoint 对其进行注解。
MainActivity.kt
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface UserManagerEntryPoint {
fun userManager(): UserManager
}
现在将 UserManager 更改为使用 UserManagerEntryPoint。
MainActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
您需要在 SettingsActivity. 中进行相同的更改。打开 SettingsActivity.kt 并替换 UserManager 的注入方式。
SettingsActivity.kt
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
3. 移除组件工厂
使用 @BindsInstance 将 Context 传递给 Dagger 组件是一种常见模式。但在 Hilt 中不需要这样做,因为 Hilt 中已提供可用作预定义绑定的 Context。
通常需要 Context 来访问资源、数据库、共享首选项等。通过使用限定符 @ApplicationContext 和 @ActivityContext,Hilt 简化了对上下文的注入。
在迁移应用时,请检查哪些类型需要 Context 作为依赖项,并用 Hilt 提供的类型进行替换。
在本例中,SharedPreferencesStorage 将 Context 作为依赖项。为了告诉 Hilt 注入上下文,请打开 SharedPreferencesStorage.kt. SharedPreferences 需要应用的 Context,因此要在上下文参数中添加 @ApplicationContext 注解。
SharedPreferencesStorage.kt
class SharedPreferencesStorage @Inject constructor(
@ApplicationContext context: Context
) : Storage {
//...
4. 迁移注入方法
接下来,您需要检查 inject() 方法的组件代码,并使用 @AndroidEntryPoint 注解相应的类。在我们的例子中,AppComponent 并未采取任何 inject() 方法,因此您无需理会。
5. 移除 AppComponent 类
由于您已经为 AppComponent.kt 中列出的所有组件添加了 EntryPoint,因此可以删除 AppComponent.kt。
6. 移除使用组件进行迁移的代码
您不再需要使用代码来初始化应用类中的自定义 AppComponent,应用类将使用由 Hilt 生成的 ApplicationComponent。移除类主体中的所有代码。结束代码应类似于下面列出的代码。
MyApplication.kt
package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application()
这样,您就成功地将 Hilt 添加到应用中,还移除了 AppComponent 并更改了 Dagger 代码以通过 Hilt 生成的 AppComponent 注入依赖项。当您在设备或模拟器上构建和测试应用时,该应用应该像往常一样正常运行。在下面几节中,我们会将各个 activity 和 fragment 迁移到 Hilt。
6. 迁移 Activity 组件
您已经迁移了应用组件并做好了基础工作,现在可以将各个组件逐个迁移到 Hilt。
我们首先来迁移登录流程。您应该使用 Hilt 为自己创建 LoginComponent 并在 LoginActivity 中加以使用,而不是手动完成此过程。
您可以遵循与上节中相同的步骤,但这次需要使用由 Hilt 生成的 ActivityComponent,因为我们将迁移由 activity 管理的组件。
首先,打开 LoginComponent.kt。LoginComponent 中没有任何模块,因此您无需执行任何操作。要使 Hilt 为 LoginActivity 生成组件并将其注入,您需要使用 @AndroidEntryPoint 注解该 activity。
LoginActivity.kt
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
//...
}
这是将 LoginActivity 迁移到 Hilt 所需添加的所有代码。由于 Hilt 将生成与 Dagger 相关的代码,因此您只需要做一些清理工作即可。删除 LoginEntryPoint 接口。
LoginActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface LoginEntryPoint {
// fun loginComponent(): LoginComponent.Factory
//}
接下来,移除 onCreate() 中的 EntryPoint 代码。
LoginActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//val entryPoint = EntryPoints.get(applicationContext, LoginActivity.LoginEntryPoint::class.java)
//entryPoint.loginComponent().create().inject(this)
super.onCreate(savedInstanceState)
...
}
由于 Hilt 将生成组件,因此请找到并删除 LoginComponent.kt。
LoginComponent 当前在 AppSubcomponents.kt 中被列为子组件。您可以安全地从子组件列表中删除 LoginComponent,因为 Hilt 会为您生成绑定。
AppSubcomponents.kt
// This module tells a Component which are its subcomponents
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
RegistrationComponent::class,
UserComponent::class
]
)
class AppSubcomponents
这就是迁移 LoginActivity 以使用 Hilt 所需的全部过程。在本节中,您删除的代码比添加的代码要多得多,这太好了!使用 Hilt 时,不仅输入的代码会更少,而且这意味着需要维护和可能引入错误的代码也会更少。
7. 迁移 Activity 和 Fragment 组件
在本节中,您将迁移注册流程。为了规划迁移,我们看一下 RegistrationComponent。打开 RegistrationComponent.kt 并向下滚动到 inject() 函数。RegistrationComponent 会负责将依赖项注入 RegistrationActivity、EnterDetailsFragment 和 TermsAndConditionsFragment。
我们先迁移 RegistrationActivity。打开 RegistrationActivity.kt 并用 @AndroidEntryPoint 注解该类。
RegistrationActivity.kt
@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
//...
}
现在 RegistrationActivity 已注册到 Hilt,您可以从 onCreate() 函数中移除 RegistrationEntryPoint 接口以及与 EntryPoint 相关的代码。
RegistrationActivity.kt
//Remove
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface RegistrationEntryPoint {
// fun registrationComponent(): RegistrationComponent.Factory
//}
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//val entryPoint = EntryPoints.get(applicationContext, RegistrationEntryPoint::class.java)
//registrationComponent = entryPoint.registrationComponent().create()
registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
Hilt 负责生成组件和注入依赖项,因此您可以移除 registrationComponent 变量和对已删除 Dagger 组件的注入调用。
RegistrationActivity.kt
// Remove
// lateinit var registrationComponent: RegistrationComponent
override fun onCreate(savedInstanceState: Bundle?) {
//Remove
//registrationComponent.inject(this)
super.onCreate(savedInstanceState)
//..
}
接下来,打开 EnterDetailsFragment.kt。与您在 RegistrationActivity 中所做的一样,用 @AndroidEntryPoint 注解 EnterDetailsFragment。
EnterDetailsFragment.kt
@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {
//...
}
由于 Hilt 将会提供依赖项,因此不需要对已删除的 Dagger 组件调用 inject()。删除 onAttach() 函数。
下一步是迁移 TermsAndConditionsFragment。像上一步一样,打开 TermsAndConditionsFragment.kt,注解该类,然后移除 onAttach() 函数。结束代码应如下所示。
TermsAndConditionsFragment.kt
@AndroidEntryPoint
class TermsAndConditionsFragment : Fragment() {
@Inject
lateinit var registrationViewModel: RegistrationViewModel
//override fun onAttach(context: Context) {
// super.onAttach(context)
//
// // Grabs the registrationComponent from the Activity and injects this Fragment
// (activity as RegistrationActivity).registrationComponent.inject(this)
//}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_terms_and_conditions, container, false)
view.findViewById<Button>(R.id.next).setOnClickListener {
registrationViewModel.acceptTCs()
(activity as RegistrationActivity).onTermsAndConditionsAccepted()
}
return view
}
}
进行此更改后,您就迁移了 RegistrationComponent 中列出的所有 activity 和 fragment,因此可以删除 RegistrationComponent.kt。
删除 RegistrationComponent 后,需要从 AppSubcomponents 的子组件列表中移除其引用。
AppSubcomponents.kt
@InstallIn(ApplicationComponent::class)
// This module tells a Component which are its subcomponents
@Module(
subcomponents = [
UserComponent::class
]
)
class AppSubcomponents
只需再完成一项操作,即可完成注册流程的迁移。注册流程声明并使用自己的作用域 ActivityScope。作用域负责控制依赖项的生命周期。在本例中,ActivityScope 指示 Dagger 在以 RegistrationActivity 开始的流程中注入 RegistrationViewModel 的相同实例。Hilt 会提供内置的生命周期作用域以支持此过程。
打开 RegistrationViewModel,将 @ActivityScope 注解更改为 Hilt 提供的 @ActivityScoped。
RegistrationViewModel.kt
@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
//...
}
由于 ActivityScope 未在其他任何地方使用,因此您可以安全地删除 ActivityScope.kt。
现在运行应用并测试注册流程。您可以使用当前的用户名和密码登录,或者注销并重新注册一个新帐号,以确认该流程是否如往常一样正常运行。
目前,Dagger 和 Hilt 正在应用中一起运行。Hilt 正在注入除 UserManager 之外的所有依赖项。在下一节中,您将通过迁移 UserManager,从 Dagger 完全迁移到 Hilt。
8. 迁移其他限定了作用域的组件
到目前为止,在本 Codelab 中,除了 UserComponent 组件外,您已成功将大部分示例应用迁移到 Hilt。UserComponent 使用自定义作用域 @LoggedUserScope 进行注解。这意味着 UserComponent 会向使用 @LoggedUserScope 注解的类注入 UserManager 的相同实例。
UserComponent 不会映射到任何可用的 Hilt 组件,因为其生命周期不是由 Android 类管理的。由于您无法在生成的 Hilt 层次结构中间添加自定义组件,因此您有两种方案可以选择:
- 让 Hilt 和 Dagger 在项目当前所处的状态中并行运行。
- 将限定了作用域的组件迁移到最接近的可用 Hilt 组件 (在本例中为
ApplicationComponent)中,并在需要时使用可为 null 性。
您在上一步中已经实现了方案 1。在这一步中,您将按照方案 2 所述,将应用完全迁移到 Hilt。但是,在实际应用中,您可以自由选择更适合您的特定用例的方案。
在此步骤中,您需要将 UserComponent 迁移为 Hilt 的 ApplicationComponent 的一部分。如果该组件中有任何模块,则也要将这些模块安装在 ApplicationComponent 中。
UserComponent 中唯一限定了作用域的类型是 UserDataRepository,该类型需要使用 @LoggedUserScope 进行注解。由于 UserComponent 将与 Hilt 的 ApplicationComponent 融合,因此 UserDataRepository 将用 @Singleton 进行注解,并且您将更改逻辑,使其在用户退出时为 null。
UserManager 已使用 @Singleton 进行注解,这意味着您可以在整个应用中提供相同的实例,并且可通过一些更改,使用 Hilt 实现相同的功能。我们先来更改 UserManager 和 UserDataRepository 的运行方式,因为您需要先做一些基础工作。
打开 UserManager.kt 并应用以下更改。
- 在构造函数中用
UserDataRepository替换UserComponent.Factory参数,因为您不再需要创建UserComponent的实例。它会以UserDataRepository作为依赖项 - 由于 Hilt 将生成组件代码,因此要删除
UserComponent及其 setter。 - 将
isUserLoggedIn()函数更改为从userRepository检查用户名,而不是检查userComponent。 - 将用户名作为参数添加到
userJustLoggedIn()函数中。 - 将
userJustLoggedIn()函数主体更改为使用userName对userDataRepository调用initData,而不是使用在迁移过程中要删除的userComponent。 - 将
username添加到registerUser()和loginUser()函数中的userJustLoggedIn()调用。 - 从
logout()函数中移除userComponent并用对userDataRepository.cleanUp()的调用进行代替。
完成后,UserManager.kt 的最终代码应如下所示。
UserManager.kt
@Singleton
class UserManager @Inject constructor(
private val storage: Storage,
// Since UserManager will be in charge of managing the UserComponent lifecycle,
// it needs to know how to create instances of it
private val userDataRepository: UserDataRepository
) {
val username: String
get() = storage.getString(REGISTERED_USER)
fun isUserLoggedIn() = userDataRepository.username != null
fun isUserRegistered() = storage.getString(REGISTERED_USER).isNotEmpty()
fun registerUser(username: String, password: String) {
storage.setString(REGISTERED_USER, username)
storage.setString("$username$PASSWORD_SUFFIX", password)
userJustLoggedIn(username)
}
fun loginUser(username: String, password: String): Boolean {
val registeredUser = this.username
if (registeredUser != username) return false
val registeredPassword = storage.getString("$username$PASSWORD_SUFFIX")
if (registeredPassword != password) return false
userJustLoggedIn(username)
return true
}
fun logout() {
userDataRepository.cleanUp()
}
fun unregister() {
val username = storage.getString(REGISTERED_USER)
storage.setString(REGISTERED_USER, "")
storage.setString("$username$PASSWORD_SUFFIX", "")
logout()
}
private fun userJustLoggedIn(username: String) {
// When the user logs in, we create populate data in UserComponent
userDataRepository.initData(username)
}
}
现在已完成对 UserManager 的处理,您需要对 UserDataRepository 进行一些更改。打开 UserDataRepository.kt 并应用以下更改。
- 移除
@LoggedUserScope,因为此依赖项将由 Hilt 管理。 UserDataRepository已注入到UserManager中,为避免循环依赖,请从UserDataRepository的构造函数中移除UserManager参数。- 将
unreadNotifications更改为可为 null,并将 setter 设为不公开。 - 添加新的可为 null 变量
username,并将 setter 设为不公开。 - 添加新函数
initData(),以将username和unreadNotifications设为随机数。 - 添加新函数
cleanUp(),以重置username和unreadNotifications计数。将username设为 null,将unreadNotifications设为 -1。 - 最后,在类主体中移动
randomInt()函数。
完成后,结束代码应如下所示。
UserDataRepository.kt
@Singleton
class UserDataRepository @Inject constructor() {
var username: String? = null
private set
var unreadNotifications: Int? = null
private set
init {
unreadNotifications = randomInt()
}
fun refreshUnreadNotifications() {
unreadNotifications = randomInt()
}
fun initData(username: String) {
this.username = username
unreadNotifications = randomInt()
}
fun cleanUp() {
username = null
unreadNotifications = -1
}
private fun randomInt(): Int {
return Random.nextInt(until = 100)
}
}
要完成 UserComponent 迁移,请打开 UserComponent.kt 并向下滚动至 inject() 方法。此依赖项可用于 MainActivity 和 SettingsActivity。我们先迁移 MainActivity。打开 MainActivity.kt 并用 @AndroidEntryPoint 注解该类。
MainActivity.kt
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
//...
}
移除 UserManagerEntryPoint 接口,并从 onCreate() 中移除与入口点相关的代码。
MainActivity.kt
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface UserManagerEntryPoint {
// fun userManager(): UserManager
//}
override fun onCreate(savedInstanceState: Bundle?) {
//val entryPoint = EntryPoints.get(applicationContext, UserManagerEntryPoint::class.java)
//val userManager = entryPoint.userManager()
super.onCreate(savedInstanceState)
//...
}
为 UserManager 声明 lateinit var,并用 @Inject 注解对其进行注解,以便 Hilt 可以注入依赖项。
MainActivity.kt
@Inject
lateinit var userManager: UserManager
由于 UserManager 将由 Hilt 注入,因此要移除对 UserComponent 的 inject() 调用。
MainActivity.kt
//Remove
//userManager.userComponent!!.inject(this)
setupViews()
}
}
这就是需要对 MainActivity 进行的所有处理。现在,您可以执行类似的更改来迁移 SettingsActivity。打开 SettingsActivity 并用 @AndroidEntryPoint 进行注解。
SettingsActivity.kt
@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
//...
}
为 UserManager 创建 lateinit var,并用 @Inject 进行注解。
SettingsActivity.kt
@Inject
lateinit var userManager: UserManager
移除入口点代码和对 userComponent() 的注入调用。完成后,onCreate() 函数应如下所示。
SettingsActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_settings)
setupViews()
}
现在,您可以清理未使用的资源以完成迁移。删除 LoggedUserScope.kt 和 UserComponent.kt 类,最后删除 AppSubcomponent.kt 类。
现在运行并再次测试应用。应用的运行情况应该像往常与 Dagger 结合使用时一样。
9. 测试
在您完成应用到 Hilt 的迁移之前,还有一个关键步骤。到目前为止,您已经迁移了所有应用代码,但尚未迁移测试。Hilt 在测试中注入依赖项,就像在应用代码中一样。使用 Hilt 进行测试不需要维护,因为 Hilt 会自动为每个测试生成一组新的组件。
单元测试
我们先进行单元测试。对于单元测试,您不需要使用 Hilt,因为您可以直接调用目标类的构造函数以传递模拟依赖项,就像构造函数没有注解一样。
如果运行单元测试,您会看到 UserManagerTest 失败。在前面几节中,您已经在 UserManager 中做了大量的工作和更改,包括处理其构造函数参数。打开仍依赖于 UserComponent 和 UserComponentFactory 的 UserManagerTest.kt。由于您已经更改了 UserManager 的参数,因此请将 UserComponent.Factory 参数更改为 UserDataRepository 的新实例。
UserManagerTest.kt
@Before
fun setup() {
storage = FakeStorage()
userManager = UserManager(storage, UserDataRepository())
}
大功告成!再次运行测试,所有单元测试都应该通过。
添加测试依赖项
开始之前,请打开 app/build.gradle 并确认存在以下 Hilt 依赖项。Hilt 会使用 hilt-android-testing 来测试特定的注解。另外,由于 Hilt 需要为 androidTest 文件夹中的类生成代码,因此其注解处理器也必须能够在此处运行。
app/build.gradle
// Hilt testing dependencies
androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
界面测试
Hilt 会为每个测试自动生成测试组件和测试应用。首先,打开 TestAppComponent.kt 以规划迁移。TestAppComponent 有两个模块:TestStorageModule 和 AppSubcomponents。您已经迁移并删除了 AppSubcomponents,可以继续迁移 TestStorageModule。
打开 TestStorageModule.kt,并用 @InstallIn 注解来进行注解。
TestStorageModule.kt
@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
//...
您已完成所有模块的迁移,请继续并删除 TestAppComponent。
接下来,将 Hilt 添加到 ApplicationTest。您必须用 @HiltAndroidTest 为任何使用 Hilt 的界面测试添加注解。此注解负责为每个测试生成 Hilt 组件。
打开 ApplicationTest.kt 并添加以下注解:
@HiltAndroidTest将指示 Hilt 为此测试生成组件。@UninstallModules(StorageModule::class)将指示 Hilt 卸载应用代码中声明的StorageModule,以便在测试期间注入TestStorageModule。- 您还需要将
HiltAndroidRule添加到ApplicationTest。此测试规则可用于管理组件的状态,并对测试执行注入。结束代码应如下所示。
ApplicationTest.kt
@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
//...
由于 Hilt 会为每个插桩测试生成一个新的 Application,因此我们需要指定在运行界面测试时应使用 Hilt 生成的 Application。为此,我们需要一个自定义的测试运行程序。
Codelab 应用已具有自定义测试运行程序。打开 MyCustomTestRunner.kt
Hilt 已附带 Application,可在名为 HiltTestApplication. 的测试中加以应用。您需要在 newApplication() 函数主体中将 MyTestApplication::class.java 更改为 HiltTestApplication::class.java。
MyCustomTestRunner.kt
class MyCustomTestRunner : AndroidJUnitRunner() {
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
}
}
进行此更改后,现在可以安全地删除 MyTestApplication.kt 文件。继续并运行测试。所有测试都应通过。
10. [可选] 迁移 ViewModel
Hilt 包含可用于从其他 Jetpack 库(如 WorkManager 和 ViewModel)提供类的扩展程序。本 Codelab 项目中的 ViewModel 是普通类,并不能扩展架构组件中的 ViewModel。在为 ViewModel 添加 Hilt 支持之前,我们需要先将应用中的 ViewModel 迁移到架构组件 ViewModel。
如需与 ViewModel 集成,您需要将以下附加依赖项添加到 gradle 文件中。我们已为您添加了这些依赖项。请注意,除了库之外,您还需要添加一个在 Hilt 注解处理器之上运行的附加注解处理器:
// app/build.gradle file
...
dependencies {
...
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:$hilt_jetpack_version'
kapt 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
kaptAndroidTest 'androidx.hilt:hilt-compiler:$hilt_jetpack_version'
}
如需将普通类迁移到 ViewModel,您需要扩展 ViewModel()。
打开 MainViewModel.kt 并添加 : ViewModel()。完成上述操作,即可迁移到架构组件 ViewModel,但是您还需要告知 Hilt 如何提供 ViewModel 的实例。为此,请在 ViewModel 的构造函数中添加 @ViewModelInject 注解。将 @Inject 注解替换为 @ViewModelInject。
MainViewModel.kt
class MainViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository
): ViewModel() {
//...
}
接下来,打开 LoginViewModel 并进行相同的更改。结束代码应如下所示。
LoginViewModel.kt
class LoginViewModel @ViewModelInject constructor(
private val userManager: UserManager
): ViewModel() {
//...
}
同样,打开 RegistrationViewModel.kt,迁移到 ViewModel(),并添加 Hilt 注解。无需使用 @ActivityScoped 注解,因为您可以使用扩展方法 viewModels() 和 activityViewModels() 控制此 ViewModel 的作用域。
RegistrationViewModel.kt
class RegistrationViewModel @ViewModelInject constructor(
val userManager: UserManager
) : ViewModel() {
进行相同的更改以迁移 EnterDetailsViewModel 和 SettingViewModel。这两个类的结束代码应如下所示。
EnterDetailsViewModel.kt
class EnterDetailsViewModel @ViewModelInject constructor() : ViewModel() {
SettingViewModel.kt
class SettingsViewModel @ViewModelInject constructor(
private val userDataRepository: UserDataRepository,
private val userManager: UserManager
) : ViewModel() {
现在,所有 ViewModel 都已迁移到架构组件 ViewModel 中,并使用 Hilt 注解进行了注解,接下来可以迁移它们的注入方式。
接下来,您需要更改 ViewModel 在视图层的初始化方式。ViewModel 由操作系统创建而成,获取它们的方法是使用 by viewModels() 委托函数。
打开 MainActivity.kt,将 @Inject 注解替换为 Jetpack 扩展程序。注意,您还需要移除 lateinit,将 var 更改为 val,并将该字段标记为 private。
MainActivity.kt
// @Inject
// lateinit var mainViewModel: MainViewModel
private val mainViewModel: MainViewModel by viewModels()
同样,打开 LoginActivity.kt 并更改 ViewModel 的获取方式。
LoginActivity.kt
// @Inject
// lateinit var loginViewModel: LoginViewModel
private val loginViewModel: LoginViewModel by viewModels()
接下来,打开 RegistrationActivity.kt 并应用类似的更改来获取 registrationViewModel。
RegistrationActivity.kt
// @Inject
// lateinit var registrationViewModel: RegistrationViewModel
private val registrationViewModel: RegistrationViewModel by viewModels()
打开 EnterDetailsFragment.kt。替换 EnterDetailsViewModel 的获取方式。
EnterDetailsFragment.kt
private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()
同样,替换 registrationViewModel 的获取方式,但是这次要使用 activityViewModels() 委托函数而非 viewModels().。注入 registrationViewModel 时,Hilt 将注入 activity 级别范围内的 ViewModel。
EnterDetailsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
打开 TermsAndConditionsFragment.kt,然后再次使用 activityViewModels() 扩展函数代替 viewModels() 来获取 registrationViewModel.
TermsAndConditionsFragment.kt
private val registrationViewModel: RegistrationViewModel by activityViewModels()
最后,打开 SettingsActivity.kt 并迁移 settingsViewModel 的获取方式。
SettingsActivity.kt
private val settingsViewModel: SettingsViewModel by viewModels()
现在运行应用,并确认一切正常。
11. 恭喜!
恭喜!您已成功将应用迁移到 Hilt!您不仅完成了迁移,而且在逐个迁移 Dagger 组件的过程中还保证了应用的正常运行。
在本 Codelab 中,您已学习如何从处理应用组件开始,建立让 Hilt 能够使用现有 Dagger 组件所必需的基础。通过对 activity 和 fragment 使用 Hilt 注解并移除与 Dagger 相关的代码,将每个 Dagger 组件迁移到 Hilt。每次完成组件迁移后,应用都会按预期工作和运行。您还使用 Hilt 提供的 @ActivityContext 和 @ApplicationContext 注解迁移了 Context 和 ApplicationContext 依赖项。您还迁移了其他 Android 组件。最后,您还迁移了测试,完成了向 Hilt 的迁移。
深入阅读
如需详细了解如何将应用迁移到 Hilt,请查看“迁移到 Hilt”文档。除了详细了解如何将 Dagger 迁移到 Hilt 之外,您还可以了解有关迁移 dagger.android 应用的信息。