在本 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 或更高版本。
获取代码
从 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 在应用中的设置如下图所示。某些类型上的点表示该类型的作用域限定为提供它的组件:
为简单起见,在您最初下载的 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 吧!
您可能很想一次将所有内容都迁移到 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 组件(链接到所有生成的组件)。
在本节中,您将迁移 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。
您已经迁移了应用组件并做好了基础工作,现在可以将各个组件逐个迁移到 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 时,不仅输入的代码会更少,而且这意味着需要维护和可能引入错误的代码也会更少。
在本节中,您将迁移注册流程。为了规划迁移,我们看一下 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。
到目前为止,在本 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 结合使用时一样。
在您完成应用到 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 文件。继续并运行测试。所有测试都应通过。
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()
现在运行应用,并确认一切正常。
恭喜!您已成功将应用迁移到 Hilt!您不仅完成了迁移,而且在逐个迁移 Dagger 组件的过程中还保证了应用的正常运行。
在本 Codelab 中,您已学习如何从处理应用组件开始,建立让 Hilt 能够使用现有 Dagger 组件所必需的基础。通过对 activity 和 fragment 使用 Hilt 注解并移除与 Dagger 相关的代码,将每个 Dagger 组件迁移到 Hilt。每次完成组件迁移后,应用都会按预期工作和运行。您还使用 Hilt 提供的 @ActivityContext
和 @ApplicationContext
注解迁移了 Context
和 ApplicationContext
依赖项。您还迁移了其他 Android 组件。最后,您还迁移了测试,完成了向 Hilt 的迁移。
深入阅读
如需详细了解如何将应用迁移到 Hilt,请查看“迁移到 Hilt”文档。除了详细了解如何将 Dagger 迁移到 Hilt 之外,您还可以了解有关迁移 dagger.android 应用的信息。