将 Dagger 应用迁移到 Hilt

在本 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 文件:

下载 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 中打开项目。
  • 点击 execute.png 运行按钮,然后选择模拟器或连接 Android 设备。此时会显示注册屏幕。

54d4e2a9bf8177c1.gif

该应用包含 4 个使用 Dagger 的不同流程(作为 activity 实现):

  • 注册:用户可以输入用户名和密码,并接受我们的条款及条件,从而完成注册。
  • 登录:用户可以使用在注册流程中添加的凭据进行登录,也可以从应用中注销。
  • 主屏幕:欢迎屏幕,用户可以查看有多少条未读通知。
  • 设置:用户可以退出并刷新未读通知的数量(这将生成随机数量的通知)。

项目遵循典型的 MVVM 模式,将视图的所有复杂性都推延到 ViewModel 中。请花点时间熟悉一下项目的结构。

8ecf1f9088eb2bb6.png

箭头表示对象之间的依赖关系。这就是我们所说的应用图:包含应用的所有类以及各个类之间的依赖关系

master 分支中的代码使用 Dagger 注入依赖项。我们将重构应用,以使用 Hilt 来生成组件和其他与 Dagger 相关的代码,而不手动创建组件。

Dagger 在应用中的设置如下图所示。某些类型上的点表示该类型的作用域限定为提供它的组件:

a1b8656d7fc17b7d.png

为简单起见,在您最初下载的 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 进行注解,并且包括两个模块:StorageModuleAppSubcomponents

AppSubcomponents 有三个组件:RegistrationComponentLoginComponentUserComponent

  • LoginComponent 注入 LoginActivity
  • RegistrationComponent 注入 RegistrationActivityEnterDetailsFragmentTermsAndConditionsFragment。此外,此组件的作用域限定为 RegistrationActivity

UserComponent 注入到 MainActivitySettingsActivity

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 注解中的模块(StorageModuleAppSubcomponents)。您需要做的第一件事是迁移这两个模块,以便 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。AppComponentRegistrationComponentLoginComponentUserManager 提供依赖项。在接下来的步骤中,您需要准备这些组件以开展迁移工作。

2. 迁移公开类型

当您将应用完全迁移到 Hilt 时,Hilt 支持您使用入口点从 Dagger 手动请求获取依赖项。通过使用入口点,您可以在迁移每个 Dagger 组件的过程中让应用保持正常运行。在此步骤中,您将在 Hilt 生成的 ApplicationComponent 中通过手动查找依赖项来替换每个 Dagger 组件。

如需从 Hilt 生成的 ApplicationComponent 中获取 RegistrationActivity.ktRegistrationComponent.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 进行类似的更改。与 RegistrationComponentLoginComponent 不同,UserManager 可同时用于 MainActivitySettingsActivity。您只需要创建一次 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. 移除组件工厂

使用 @BindsInstanceContext 传递给 Dagger 组件是一种常见模式。但在 Hilt 中不需要这样做,因为 Hilt 中已提供可用作预定义绑定Context

通常需要 Context 来访问资源、数据库、共享首选项等。通过使用限定符 @ApplicationContext@ActivityContext,Hilt 简化了对上下文的注入。

在迁移应用时,请检查哪些类型需要 Context 作为依赖项,并用 Hilt 提供的类型进行替换。

在本例中,SharedPreferencesStorageContext 作为依赖项。为了告诉 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 会负责将依赖项注入 RegistrationActivityEnterDetailsFragmentTermsAndConditionsFragment

我们先迁移 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 层次结构中间添加自定义组件,因此您有两种方案可以选择:

  1. 让 Hilt 和 Dagger 在项目当前所处的状态中并行运行。
  2. 将限定了作用域的组件迁移到最接近的可用 Hilt 组件 (在本例中为 ApplicationComponent)中,并在需要时使用可为 null 性。

您在上一步中已经实现了方案 1。在这一步中,您将按照方案 2 所述,将应用完全迁移到 Hilt。但是,在实际应用中,您可以自由选择更适合您的特定用例的方案。

在此步骤中,您需要将 UserComponent 迁移为 Hilt 的 ApplicationComponent 的一部分。如果该组件中有任何模块,则也要将这些模块安装在 ApplicationComponent 中。

UserComponent 中唯一限定了作用域的类型是 UserDataRepository,该类型需要使用 @LoggedUserScope 进行注解。由于 UserComponent 将与 Hilt 的 ApplicationComponent 融合,因此 UserDataRepository 将用 @Singleton 进行注解,并且您将更改逻辑,使其在用户退出时为 null。

UserManager 已使用 @Singleton 进行注解,这意味着您可以在整个应用中提供相同的实例,并且可通过一些更改,使用 Hilt 实现相同的功能。我们先来更改 UserManagerUserDataRepository 的运行方式,因为您需要先做一些基础工作。

打开 UserManager.kt 并应用以下更改。

  • 在构造函数中用 UserDataRepository 替换 UserComponent.Factory 参数,因为您不再需要创建 UserComponent 的实例。它会以 UserDataRepository 作为依赖项
  • 由于 Hilt 将生成组件代码,因此要删除 UserComponent 及其 setter。
  • isUserLoggedIn() 函数更改为从 userRepository 检查用户名,而不是检查 userComponent
  • 将用户名作为参数添加到 userJustLoggedIn() 函数中。
  • userJustLoggedIn() 函数主体更改为使用 userNameuserDataRepository 调用 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(),以将 usernameunreadNotifications 设为随机数。
  • 添加新函数 cleanUp(),以重置 usernameunreadNotifications 计数。将 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() 方法。此依赖项可用于 MainActivitySettingsActivity。我们先迁移 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 注入,因此要移除对 UserComponentinject() 调用。

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 中做了大量的工作和更改,包括处理其构造函数参数。打开仍依赖于 UserComponentUserComponentFactory 的 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 有两个模块:TestStorageModuleAppSubcomponents。您已经迁移并删除了 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() {

进行相同的更改以迁移 EnterDetailsViewModelSettingViewModel。这两个类的结束代码应如下所示。

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 注解迁移了 ContextApplicationContext 依赖项。您还迁移了其他 Android 组件。最后,您还迁移了测试,完成了向 Hilt 的迁移。

深入阅读

如需详细了解如何将应用迁移到 Hilt,请查看“迁移到 Hilt”文档。除了详细了解如何将 Dagger 迁移到 Hilt 之外,您还可以了解有关迁移 dagger.android 应用的信息。