8

Dagger之十九、Hilt 迁移实战

 2 years ago
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 迁移实战

作者: rain 分类: Android Training 发布时间: 2021-10-30 13:23 6 0条评论

本节课通过一个实际使用 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 ,在该部件中包含了两个模块 StorageModuleAppSubcomponentsAppSubcomponents 是一个聚合模块接口,里面包含了另外三个子部件,该模块存在的意义就是把这三个子部件安装到 AppComponent 这个父部件上,在三个子部件分别是 RegistrationComponentLoginComponentUserComponent,每个部件的功能如下:
– LoginComponent 用来负责注入 LoginActivity 中的 @Inject 变量
– RegistrationComponent 用来负责注入 RegistrationActivityEnterDetailsFragmentTermsAndConditionsFragment 三个类里面的 @Inject 变量。该部件使用了自定义的 @ActivityScopeRegistrationActivity 实例绑定到了一起,意味着在 RegistrationActivity 实例中该部件中的 ActivityScope 对象只有一个实例。
– UserComponent 用来负责注入 MainActivitySettingsActivity 中的 @Inject 变量

现在我们对该应用的 Dagger 部件有了大概的了解,下面可以开始动手迁移了。

迁移 Application 部件

首先来迁移应用里面的 AppComponent 部件,在迁移的过程中需要做一些额外的工作来保证其他 Dagger 代码可以正常工作。

首先打开 MyApplication.kt 这个文件,在里面定义了 MyApplication 这个继承自 Application 的类,在该类上添加 @HiltAndroidApp 注解,这样就为该应用启用了 Hilt 。

MyApplication.kt

package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
// 在自定义的 Application 中添加 HiltAndroidApp 注解
@HiltAndroidApp
open class MyApplication : Application() {
    // 这个 AppComponent 部件对象被该项目中的其他 Activity 使用,
    // 作为其他 Activity 子部件的父部件
    val appComponent: AppComponent by lazy {
        initializeComponent()
    open fun initializeComponent(): AppComponent {
        // 使用工厂方法来创建一个 AppComponent 对象,
        // 由于在该部件中需要 Context 对象,所以把 applicationContext
        // 对象当做参数来创建该部件实例
        return DaggerAppComponent.factory().create(applicationContext)

如果你的 Kotlin 语法不太了解的话, 上面的 return DaggerAppComponent.factory().create(applicationContext) 这行代码中的 applicationContext 等价于调用 getApplicationContext() 这个函数,返回值是一个 Application Context 对象。

迁移部件上的模块

打开 AppComponent.kt 文件可以发现 AppComponent 部件中定义了两个模块 StorageModuleAppSubcomponents。现在开始迁移这两个模块,把他们安装到 Hilt 中的 ApplicationComponent 部件中。

打开 AppSubcomponents.kt 文件,并使用 @InstallIn(ApplicationComponent::class) 注解

// 该模块告诉父部件有那几个子部件
// 通过 InstallIn 注解把该模块安装到 Hilt 所生成的 ApplicationComponent 部件中,
// 现在 ApplicationComponent 部件变成了这几个子部件的父部件
@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        LoginComponent::class,
        UserComponent::class
class AppSubcomponents

ApplicationComponent::class 是 Kotlin 语言中用来引用 Java class 的语法。

对于 StorageModule 做同样的操作,打开 StorageModule.kt 文件:

// 这是一个普通的 Dagger 模块
// 把该模块安装到 Hilt 所生成的 ApplicationComponent 部件中
@InstallIn(ApplicationComponent::class)
@Module
abstract class StorageModule {
    // 当有人需要注入 Storage 类型的对象的时候, Dagger 生成一个 SharedPreferencesStorage 实例
    @Binds
    abstract fun provideStorage(storage: SharedPreferencesStorage): Storage

现在在回去看一下 AppComponent.kt 文件中定义的 AppComponent 模块,该模块为 RegistrationComponentLoginComponentUserManager 提供依赖对象,下一步我们就准备迁移这些子部件。

@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
    // 创建 AppComponent 部件的工厂类定义
    @Component.Factory
    interface Factory {
        // 使用 @BindsInstance 把 Context 对象添加到部件中
        fun create(@BindsInstance context: Context): AppComponent
    // 从部件对象关系图中可以获取到的类
    fun registrationComponent(): RegistrationComponent.Factory
    fun loginComponent(): LoginComponent.Factory
    fun userManager(): 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

class RegistrationActivity : AppCompatActivity() {
    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface RegistrationEntryPoint {
        fun registrationComponent(): RegistrationComponent.Factory

然后把 Activity 中获取 RegistrationComponent.Factory 对象的代码修改为使用 RegistrationEntryPoint 来获取子部件:

RegistrationActivity.kt

        // 从 Hilt 所生成的 Application 部件中获取上面定义的 Entry points
        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, RegistrationEntryPoint::class.java)
        // 调用 Entry ponits 上的 registrationComponent() 函数来创建工厂对象
        registrationComponent = entryPoint.registrationComponent().create()

通过上面的修改,现在在 Activity 中可以从 Hilt 生成的部件中获取 Dagger 子部件了。通过 Entry Points 把 Dagger 代码和 Hilt 代码连接到了一起。

现在按照上面同样的方式给其他 AppComponent 对外暴露的对象添加一个 Entry Points接口。

继续修改使用 LoginComponent.Factory 的地方,打开 LoginActivity.kt 文件并创建一个 Entry Points 接口:

LoginActivity.kt

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface LoginEntryPoint {
        fun loginComponent(): LoginComponent.Factory

继续修改 LoginActivity.kt 中获取 LoginComponent 对象并实现注入的代码:

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, LoginEntryPoint::class.java)
        entryPoint.loginComponent().create().inject(this)

现在 AppComponent 对外暴露的三个类型中,有两个已经替换成了 Hilt 所支持的 Entry Points 实现。现在就剩 UserManager 这个类型了,这个类型和 RegistrationComponentLoginComponent 比起来有点不一样,该类被 MainActivitySettingsActivity 这两个类同时使用。我们不需要在每个 Activity 中都创建一个 Entry Points,只需要创建一个就可以了。我们可以先把该接口定义到 MainActivity 中:

打开 MainActivity.kt 文件来创建这个 Entry Points:

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface UserManagerEntryPoint {
        fun userManager(): UserManager

然后在 MainActivity.ktSettingsActivity.kt 中分别修改使用 UserManager 的代码:
MainActivity.kt

        val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
        val userManager = entryPoint.userManager()

SettingsActivity.kt

    val entryPoint = EntryPointAccessors.fromApplication(applicationContext, MainActivity.UserManagerEntryPoint::class.java)
    val userManager = entryPoint.userManager()

Entry Points 通常创建在使用他们的地方。如果一个 Entry Points 被用于多个地方,也可以选择把 Entry Points 定义到一个新的类中,并把该类放到一个类似于 util 这种 Java 包下。

删除部件工厂类定义

在部件工厂类中还使用了 @BindsInstance 来把 Context 对象添加到了部件中,这说明当前有依赖对象需要使用 Context 对象,经过分析我们发现 SharedPreferencesStorage 类的构造函数需要 Context 对象,在 Hilt 中默认已经在部件中支持了 Context 对象,现在修改 SharedPreferencesStorage 函数来使用 Hilt 所提供的 Context 对象:

SharedPreferencesStorage.kt

class SharedPreferencesStorage @Inject constructor(
    @ApplicationContext context: Context
) : Storage {
//...

检测部件中的对象注入函数

现在看看部件中定义了哪些对象注入函数(inject() 函数),然后在需要注入的类上使用 @AndroidEntryPoint 注解。在示例项目中的 AppComponent 部件中并没有定义 inject() 函数,所以这一步我们啥都不需要做。

删除 AppComponent 类

前面已经为 AppComponent 类中的所有所提供的对象都创建了 EntryPoints 接口,现在可以删除 AppComponent.kt 了。

删除使用 AppComponent 的代码

现在在 Application 类中不再需要初始化 AppComponent 部件对象了,可以删除 Application 类中的相关代码了,删除后该类的代码如下:

MyApplication.kt

package com.example.android.dagger
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
open class MyApplication : Application()

现在已经成功的把 Hilt 应用到了项目中的 Application 上,之前定义的 AppComponent 部件已经完全被 Hilt 生成的 ApplicationComponent 替代了。

现在编译并运行该应用,该应用应该可以继续正确的运行。下一步继续迁移 Activity 和 Fragment。

迁移 Activity 部件

先来迁移登录流程,当前 LoginActivity 是通过 Entry Point 来获取 LoginComponent 的,需要在 Activity 中调用我们自己编写的代码来实现对 Activity 的注入,我们想让 Hilt 来自动实现该功能。

打开 LoginComponent.kt 文件发现 LoginComponent 并没有包含任何 Dagger 模块,所以这里针对模块不需要做任何事情,只需要在 LoginActivity 类上添加 @AndroidEntryPoint 即可:

@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
    //...

这样 Hilt 就可以管理 LoginActivity 类了,我们之前定义的 LoginEntryPoint 和使用该接口的代码都可以删除了。

LoginActivity.kt

    //删除定义 LoginEntryPoint 的代码
    //@InstallIn(ApplicationComponent::class)
    //@EntryPoint
    //interface LoginEntryPoint {
    //    fun loginComponent(): LoginComponent.Factory
override fun onCreate(savedInstanceState: Bundle?) {
   //删除 onCreate 函数中使用 LoginEntryPoint 的代码
   //val entryPoint = EntryPoints.get(applicationContext, LoginActivity.LoginEntryPoint::class.java)
   //entryPoint.loginComponent().create().inject(this)
    super.onCreate(savedInstanceState)

最后删除 LoginComponent.kt 文件,同时从 AppSubcomponents.kt 中删除对 LoginComponent 的引用:

@InstallIn(ApplicationComponent::class)
@Module(
    subcomponents = [
        RegistrationComponent::class,
        UserComponent::class
class AppSubcomponents

上面就是把 LoginActivity 迁移到 Hilt 所需要做的改动。上面的改动主要是删除了大量的代码,然后 Hilt 自动生成我们需要的东西。下面继续迁移 Fragment 。

迁移 Fragment 部件

这一步来迁移注册流程。在迁移之前先来看看 RegistrationComponent 部件,打开 RegistrationComponent.kt 文件找到里面所定义的 inject() 函数(成员注入函数),可以看到该部件中定义了三个该函数,这意味着该部件负责注入 RegistrationActivityEnterDetailsFragmentTermsAndConditionsFragment 这三个类中的 @Inject 变量。

先从 RegistrationActivity 类开始,打开 RegistrationActivity.kt 文件给该类添加 @AndroidEntryPoint 注解:

RegistrationActivity.kt

@AndroidEntryPoint
class RegistrationActivity : AppCompatActivity() {
    //...

这样该类就交给 Hilt 来管理了,然后就可以和 LoginActivity 一样删除 RegistrationEntryPoint 接口以及使用该 Entry Points 的相关代码了。

RegistrationActivity.kt

//删除 RegistrationEntryPoint 接口
//@InstallIn(ApplicationComponent::class)
//@EntryPoint
//interface RegistrationEntryPoint {
//    fun registrationComponent(): RegistrationComponent.Factory
override fun onCreate(savedInstanceState: Bundle?) {
    //删除使用 RegistrationEntryPoint 的代码
    //val entryPoint = EntryPoints.get(applicationContext, RegistrationEntryPoint::class.java)
    //registrationComponent = entryPoint.registrationComponent().create()
    registrationComponent.inject(this)
    super.onCreate(savedInstanceState)

现在该类中的需要注入的变量由 Hilt 自动完成注入,所以不需要该类中的 registrationComponent 变量了,也不需要通过手工的调用该 registrationComponent 子部件来注入这个类了。

RegistrationActivity.kt

// 删除这个子部件变量
// lateinit var registrationComponent: RegistrationComponent
override fun onCreate(savedInstanceState: Bundle?) {
    //删除调用子部件执行注入的函数
    //registrationComponent.inject(this)
    super.onCreate(savedInstanceState)

修改完了 Activity ,继续打开 EnterDetailsFragment.kt 这个文件,在 EnterDetailsFragment 类上面使用 @AndroidEntryPoint 注解。

EnterDetailsFragment.kt

@AndroidEntryPoint
class EnterDetailsFragment : Fragment() {
    //...

这样该 Fragment 中需要注入的变量也由 Hilt 来注入,所以该 Fragment 里面的 onAttach() 可以删除了。

然后继续迁移 TermsAndConditionsFragment 类,打开 TermsAndConditionsFragment.kt 文件,在该类上使用 @AndroidEntryPoint 注解并删除 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 都迁移到 Hilt 了,所以可以删除 RegistrationComponent.kt 文件了,同时从 AppSubcomponents 中删除对该子部件的引用:

AppSubcomponents.kt

@InstallIn(ApplicationComponent::class)
// 该模块告诉部件它有几个子部件
@Module(
    subcomponents = [
        UserComponent::class
class AppSubcomponents

现在还差最后一步就完成了对注册流程的迁移。这最后一步就是自定义范围(Scope),在注册部件上使用了自定义的 ActivityScope,自定义范围是用来控制部件中依赖对象的生命周期的。在这个示例中,ActivityScope 告诉 Dagger 在 RegistrationActivity 这个 Activity 的生命周期内,注入的 RegistrationViewModel 对象都是同一个实例。 Hilt 已经预定义了对应的自定义范围了,这里我们可以使用 Hilt 预定义的 @ActivityScoped,打开 RegistrationViewModel 文件,把上面的 @ActivityScope 注解替换为 @ActivityScoped

RegistrationViewModel.kt

@ActivityScoped
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
    //...

上面的 @Inject constructor 是 Kotlin 定义构造函数的语法,相当于在 Java 里面的构造函数上使用了 @Inject 注解。

现在自定义的 ActivityScope 没有人使用了,可以安全的删除这个类了。

现在编译应用并运行,你可以继续使用之前已经注册的用户名和密码来登录,也可以注销账号从新注册一个,应用行为表现正常。

现在在该应用中同时在使用 Hilt 和 Dagger ,除了 UserManager 这个依赖对象之外,其他的对象都是 Hilt 来注入和管理的。下一步来看看这个 UserManager 如何迁移。

如果这一步你的代码有问题,则可以下载正确的代码 来和你的代码做下对比,看看哪一步没有迁移正确。

迁移另一个范围部件

现在就剩 UserComponent 这最后一个部件还没迁移到 Hilt 了。该部件使用了一个自定义的 @LoggedUserScope 范围注解。这意味着,对于使用了 @LoggedUserScope 注解的类 UserComponent 部件会使用同一个 UserManager 对象来注入这些类。

UserComponent 没有对应的 Hilt 部件,该部件的生命周期和安卓组件的生命周期不一致。由于Hilt不支持把自定义部件添加到 Hilt 所生成的部件层级中,所以需要对该子部件功能做下调整,你可以有两个选择:

  1. 同时使用 Dagger 子部件和 Hilt,就像目前的代码一样,让他们同时存在一个项目中。
  2. 把该自定义范围子部件中的对象迁移到最接近该子部件的 Hilt 部件中(这该示例项目中是 ApplicationComponent 部件),当需要的时候可以设置该部件中依赖对象为 null 值。

现在来看看如何实现第二个选项,把该项目完全迁移到 Hilt。

我们选择把 UserComponent 迁移到 Hilt 生成的 ApplicationComponent 中,所以该部件中的模块需要安装到 ApplicationComponent 中。

UserComponent 模块中只有一个使用了自定义范围 LoggedUserScope 注解的范围对象 — UserDataRepository。由于我们把该子部件合并到了 ApplicationComponent 中,所以 UserDataRepository 上面的范围注解需要替换为 @Singleton,并且修改代码逻辑当用户没有登录的时候让该对象的值为 null。

UserManager 已经使用了 @Singleton,说明在整个应用生命周期中,获得到的 UserManager 对象都是一样的,对代码做些修改,可以实现前面同样的功能。在动手修改之前,先要对 UserManagerUserDataRepository 的功能做下调整。

打开 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 代码如下:

@Singleton
class UserManager @Inject constructor(
    private val storage: Storage,
    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 文件,做如下修改:

  • 现在依赖对象都通过 Hilt 来管理,可以删除 LoggedUserScope 注解了
  • UserManager 中已经需要依赖 UserDataRepository 对象了,为了避免循环依赖,可以删除 UserDataRepository 构造函数上的 UserManager 参数了
  • unreadNotifications 变量设置为可以为 null 值,并且把 setter 函数设置为 private
  • 添加一个新的变量 username,该变量可以为 null 值,并且把 setter 函数设置为 private
  • 添加一个 initData() 函数,该函数设置 usernameunreadNotifications 这两个变量
  • 添加一个 cleanUp() 函数,该函数重置 usernameunreadNotifications 这两个变量的值
  • 最后把 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)

上面的修改相当于修改了如何判断用户是否登录的逻辑,现在不是通过 UserDataRepository 对象是否为 null 来判断了,而是通过用户名是否为 null 来判断,这样 Hilt 就可以注入 UserDataRepository 对象了,而不用像前面一样,当用户登录了才去注入。

现在可以开始迁移 UserComponent.kt 了,打开该类文件,找到 inject() 函数,发现该部件在 MainActivitySettingsActivity 中使用。先来迁移 MainActivity,打开该类在上面添加 @AndroidEntryPoint 注解:

MainActivity.kt

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    //...

删除里面的 UserManagerEntryPoint 接口和相关的代码。

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 变量:

MainActivity.kt

@Inject
lateinit var userManager: UserManager

现在这个 UserManager 对象通过 Hilt 来注入,可以删除调用 UserComponent#inject() 这个函数的代码了

MainActivity.kt

        //删除下面一行代码
        //userManager.userComponent!!.inject(this)
        setupViews()

上面是 MainActivity 中需要做的修改,现在继续修改 SettingsActivity 类,先在上面添加 @AndroidEntryPoint 并同样添加一个 UserManager 对象:

@AndroidEntryPoint
class SettingsActivity : AppCompatActivity() {
    @Inject
    lateinit var userManager: UserManager
    //...

然后删除调用 userComponent 相关的代码,修改完以后 SettingsActivityonCreate 函数应该是这样的:

SettingsActivity.kt

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)
        setupViews()

现在可以删除 LoggedUserScope.kt、UserComponent.kt 和 AppSubcomponent.kt 这些 Dagger 中所定义的代码了。

现在整个应用全部迁移到 Hilt 了,编译安装该应用可以看到起功能和之前使用 Dagger 时候的情况是一样的,迁移是成功的。

现在应用代码已经迁移到 Hilt 了,就差测试代码了。 先来看看单元测试如何使用 Hilt。

在测试中你也可以选择不用 Hilt 来注入对象,可以直接手工的使用构造函数来创建对象。比如我们的 UserManagerTest 单元测试就是这样的,目前的 UserManagerTest 单元测试无法执行,原因在于我们对 UserManager 的逻辑做了修改,所以需要修改一下相关的测试代码:

UserManagerTest.kt

    @Before
    fun setup() {
        storage = FakeStorage()
        userManager = UserManager(storage, UserDataRepository())

修改后再运行代码,该测试应该就可以通过了。

添加测试依赖对象

在进入 Hilt 测试之前,先打开 应用的 app/build.gradle 配置文件,在里面添加 Hilt 测试库 :

    // Hilt 测试依赖项
    androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
    kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"

由于在测试中,Hilt 也要用来生成需要的类,所以需要添加 kaptAndroidTest

Hilt 会自动的为每个测试案例生成测试的部件和测试的 Application。打开 TestAppComponent.kt 文件来开始迁移到 Hilt。TestAppComponent 有两个模块 TestStorageModuleAppSubcomponents,在前面的迁移中已经删除了 AppSubcomponents 类,现在可以继续迁移 TestStorageModule 模块。

打开 TestStorageModule.kt 类,在该类上使用 @InstallIn(ApplicationComponent::class) 注解:

TestStorageModule.kt

@InstallIn(ApplicationComponent::class)
@Module
abstract class TestStorageModule {
    //...

现在所有的测试模块也迁移完毕了,可以去删除 TestAppComponent 类了。

下面可以把 Hilt 添加到 ApplicationTest 中了,先把所有使用 Hilt 的 UI 测试 修改为使用 @HiltAndroidTest 注解。该注解告诉 Hilt 为每个测试生成 Hilt 部件。

打开 ApplicationTest.kt 文件添加如下的注解:
@HiltAndroidTest 告诉 Hilt 为测试生成需要的部件
@UninstallModules(StorageModule::class) 告诉 Hilt 把 StorageModule 从依赖部件中移除,在测试的过程中使用 TestStorageModule 模块替代
– 在 ApplicationTest 中添加 HiltAndroidRule,该 Rule 用来管理部件的状态,还用来注入测试需要的对象。最终修改后的代码应该是这样的:

ApplicationTest.kt

@UninstallModules(StorageModule::class)
@HiltAndroidTest
class ApplicationTest {
    @get:Rule
    var hiltRule = HiltAndroidRule(this)
    //...

当执行测试的时候,还需要告诉测试框架使用 Hilt 所生成的 Application,通过自定义 test runner 来实现这个需求。在该示例项目中已经定义了一个自定义的 Test runner,打开 MyCustomTestRunner.kt 文件。在 test runner 的 newApplication 函数中使用 Hilt 提供的测试 Application 即可:

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 文件了。然后运行下测试,所有的测试应该都可以通过。

使用 Jetpack 中的 ViewModel

Hilt 目前还支持 Jetpack 库中的 WorkManager 和 ViewModel。在该示例项目中,我们的 ViewModel 并没有继承自 Jetpack 中的 ViewModel 类。为了演示 Hilt 对ViewModel 的支持,我们可以把项目中的 MainViewModel 修改为继承自 Jetpack 中的 VIewModel。

在使用 ViewModel 之前需要现在 app/build.gradle 配置文件中添加需要的库:

// 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'

然后打开 MainViewModel.kt 类,让该类继承自 Jetpack 中的 ViewModel。然后在该类的构造函数上使用 @ViewModelInject 注解这样 Hilt 就知道如何创建这个 ViewModel 对象了。

MainViewModel.kt

class MainViewModel @ViewModelInject constructor(
    private val userDataRepository: UserDataRepository
): ViewModel() {
//...

对于 LoginViewModel 类做同样的修改

LoginViewModel.kt

class LoginViewModel @ViewModelInject constructor(
    private val userManager: UserManager
): ViewModel() {
//...

对于 RegistrationViewModel 类也是如此,通过扩展函数 viewModels() 和 activityViewModels() 已经可以控制 ViewModel 的生命周期了所以在 RegistrationViewModel 中不需要 @ActivityScoped 注解了。

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 类都迁移到 Jetpack ViewModel 了。可以开始迁移他们被注入的代码了。

在 Jetpack 中,ViewModel 是通过 by viewModels() 代理函数来获取到的。

打开 MainActivity.kt 来修改定义 ViewModel 的变量 mainViewModel,现在由于该变量是通过代理函数赋值的,所以可以设置为 private 的:

by viewModels() 是 Kotlin 提供的工具扩展函数,类似于 Java 里面助手类,可以用来获取该 Activity 中的ViewModel 对象

MainActivity.kt

//    @Inject
//    lateinit var mainViewModel: MainViewModel
    private val mainViewModel: MainViewModel by viewModels()

对于其他几个 Activity 也需要同样的修改:

LoginActivity.kt

//    @Inject
//    lateinit var loginViewModel: LoginViewModel
    private val loginViewModel: LoginViewModel by viewModels()

RegistrationActivity.kt

//    @Inject
//    lateinit var loginViewModel: LoginViewModel
    private val loginViewModel: LoginViewModel by viewModels()

RegistrationActivity.kt

//    @Inject
//    lateinit var registrationViewModel: RegistrationViewModel
    private val registrationViewModel: RegistrationViewModel by viewModels()

EnterDetailsFragment.kt

    private val enterDetailsViewModel: EnterDetailsViewModel by viewModels()

SettingsActivity.kt

private val settingsViewModel: SettingsViewModel by viewModels()

由于下面两个 Fragment 之前使用的是 Activity 范围的对象,所以这里通过 activityViewModels() 代理函数来注入 Activity 里面的 ViewModel 对象,相当于 这两个 Fragemnt 中所使用的 ViewModel 对象的依赖对象是 Activity 中共享的对象。

EnterDetailsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

TermsAndConditionsFragment.kt

    private val registrationViewModel: RegistrationViewModel by activityViewModels()

这样就把所有的 ViewModel 都迁移到 Jetpack ViewModel 了,并且使用 Hilt 来创建每个 ViewModel。

通过一步一步的 把 Dagger 项目迁移到 Hilt,可以看出在迁移 Dagger 项目的时候,还是需要详细规划每步应该迁移那个组件,特别是哪些和安卓组件没有对应关系的自定义组件的迁移,遇到这种情况,需要修改一下业务逻辑保证能够把这些组件提供的功能放到对应的安卓组件中,这样才能完全迁移到 Hilt 框架中,如果你之前的代码业务很复杂,短期内无法迁移这些子部件,那么就只能让 Hilt 和 Dagger子部件共存了。

而对于刚刚开始使用 Hilt 的新项目则不存在这个问题,并且 Hilt 在使用的时候更具规范和简单。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK