Jetpack 新成员 Hilt 实践(一)启程过坑记

13,135 阅读16分钟

前言

在之前的文章里面分别分析 Jetpack 新成员 App Startup 实践以及原理分析Jetpack 新成员 Paging3 实践以及源码分析(一) 以及 Jetpack 新成员 Paging3 网络实践及原理分析(二) 如果没有看过可以点击下方地址前去查看:

这篇文章主要来分析一下 Hilt,花了好几天时间梳理了一下 官方 Hilt 文档,Hilt 的知识点有点多,将会分为三篇文章结合实际案例来完成,每篇文章都会有详细的使用的案例。本篇文章的案例已经上传到了 GitHub:HiltSimple

通过这篇文章你将学习到以下内容:

  • 为什么需要使用依赖注入库?
  • Hilt 是什么?
  • Hilt 常用注解含义?
  • 使用 Hilt 都有那些坑需要注意?
  • Hilt 如何和 Android 组件一起使用?
  • Hilt 如何和第三方组件一起使用?
  • Hilt 如何和 Jetpack 组件 ViewModule 一起使用?
  • Hilt 如何和 Jetpack 组件 Room 一起使用?

研究 Hilt 时遇到一些坑,有些坑在 Goggle 文档上也没有提到,我会在文中特别强调,并在 文末总结部分 进行汇总。

为什么需要使用依赖注入库

Hilt、Dagger、Koin 等等都是依赖注入库,Google 也在努力不断的完善依赖注入库从 Dagger 到 Dagger2 在到现在的 Hilt,因为依赖注入是面向对象设计中最好的架构模式之一,使用依赖注入库有以下优点:

  • 依赖注入库会自动释放不再使用的对象,减少资源的过度使用。
  • 在配置 scopes 范围内,可重用依赖项和创建的实例,提高代码的可重用性,减少了很多模板代码。
  • 代码变得更具可读性。
  • 易于构建对象。
  • 编写低耦合代码,更容易测试。

Hilt 是什么?

Hilt 是 Android 的依赖注入库,它减少了在项目中进行手动依赖,进行手动依赖注入需要您手动构造每个类及其依赖,依赖注入库的出现节省了 Android 开发者大量的时间。

Hilt 通过为项目中的每个 Android 类提供容器并自动管理它们的生命周期,提供了在应用程序中使用 DI 的标准方法。Hilt 是在 Dagger 的基础上进行构建,因为 Dagger 提供的编译时正确性、运行时性能、可伸缩性并且从 Android Studio 支持 Dagger 中获益。

Hilt 的实现要比 Dagger 简单得多,使用 Dagger 实现依赖注入,需要去编写 modules、components 等等。每次创建一个新的 android 组件,比如 Activity、Fragment 或Service,我们都需要手动将它们添加到各自的 modules 中,以便在需要的时候注入它们。

接下来我们开始从分析 Hilt 注解的含义出发,来了解如何在应用程序中使用 Hilt。

Hilt 常用注解的含义

Hilt 常用注解包含 @HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides、@EntryPoint 等等。

@HiltAndroidApp

  1. 所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application。
  2. @HiltAndroidApp 注解将会触发 Hilt 代码的生成,作为应用程序依赖项容器的基类。
  3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖。
  4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,组件包含Application、Activity、Fragment、View、Service、BroadcastReceiver 等等。

@AndroidEntryPoint

Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)特殊的 Application 使用 @HiltAndroidApp 注解。

  • Activity:仅仅支持 ComponentActivity 的子类例如 FragmentActivity、AppCompatActivity 等等。
  • Fragment:仅仅支持继承 androidx.Fragment 的 Fragment
  • View
  • Service
  • BroadcastReceiver

坑:

  • 如果使用 @AndroidEntryPoint 在非 ComponentActivity 子类上注解,例如 Activity 则会抛出以下异常。

    Activities annotated with @AndroidEntryPoint must be a subclass of androidx.activity.ComponentActivity. (e.g. FragmentActivity, AppCompatActivity, etc.)
    
  • 如果使用 @AndroidEntryPoint 注解 Android 类,必须在它依赖的 Android 类添加同样的注解,例如在 Fragment 中添加 @AndroidEntryPoint 注解,必须在 Fragment 依赖的 Activity 上也添加 @AndroidEntryPoint 注解 , 否则会抛出以下异常。

    java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
    

@Inject

Hilt 需要知道如何从相应的组件中提供必要依赖的实例。使用 @Inject 注解来告诉 Hilt 如何提供该类的实例,它常用于构造函数、非私有字段、方法中。

注意:在构建时,Hilt 为 Android 类生成 Dagger 组件。然后 Dagger 遍历您的代码并执行以下步骤:

  • 构建并验证依赖关系,确保没有未满足的依赖关系。
  • 生成它在运行时用于创建实际对象及其依赖项的类。

@Module

常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等),使用 @Module 注解的类,需要使用 @InstallIn 注解指定 module 的范围。

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {
}

@InstallIn

使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围,例如使用 @InstallIn(ActivityComponent::class) 注解的 module 会绑定到 activity 的生命周期上。

Hilt 提供了以下组件来绑定依赖与 对应的 Android 类的活动范围。

Hilt 提供的组件对应的 Android 类的活动范围
ApplicationComponentApplication
ActivityRetainedComponentViewModel
ActivityComponentActivity
FragmentComponentFragment
ViewComponentView
ViewWithFragmentComponentView annotated with @WithFragmentBindings
ServiceComponentService

注意:Hilt 没有为 broadcast receivers 提供组件,因为 Hilt 直接从 ApplicationComponent 注入 broadcast receivers。

Hilt 会根据相应的 Android 类生命周期自动创建和销毁生成的组件类的实例,它们的对应关系如下表格所示。

Hilt 提供的组件创建对应的生命周期销毁对应的生命周期
ApplicationComponentApplication#onCreate()Application#onDestroy()
ActivityRetainedComponentActivity#onCreate()Activity#onDestroy()
ActivityComponentActivity#onCreate()Activity#onDestroy()
FragmentComponentFragment#onAttach()Fragment#onDestroy()
ViewComponentView#super()View destroyed
ViewWithFragmentComponentView#super()View destroyed
ServiceComponentService#onCreate()Service#onDestroy()

@Provides

它常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {

    /**
     * @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
     * @Singleton 提供单例
     */
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .build()
    }
}

@EntryPoint

Hilt 支持最常见的 Android 类 Application、Activity、Fragment、View、Service、BroadcastReceiver 等等,但是您可能需要在Hilt 不支持的类中执行依赖注入,在这种情况下可以使用 @EntryPoint 注解进行创建,Hilt 会提供相应的依赖。

基本概念介绍完了之后,我们正式在项目中使用 Hilt。

如何使用 Hilt

首先需要添加 Hilt 依赖,Hilt 依赖添加方式相比于 Koin 太麻烦了,首先在 project 的 build.gradle 添加以下依赖。

buildscript {
    ...
    dependencies {
        ...
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
    }
}

然后在 App 模块中的 build.gradle 文件中添加以下代码。

...
apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:2.28-alpha"
    kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
}

坑:需要注意的是如果同时使用 hilt 和 data binding,Android Studio 的版本必须 >= 4.0

所以还没有升级的朋友们,尽快升级吧,升级到 Android Studio 4.0 也会遇到一些坑,不过好在这些坑现在都有相应的解决方案了。

Hilt 使用 Java 8 的功能,所以要在项目中启用 Java 8,需要在 App 模块的 build.gradle 文件中,添加以下代码

android {
  ...
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    // For Kotlin projects
    kotlinOptions {
        jvmTarget = "1.8"
    }
}

注意: 这里有一个坑,对于 Kotlin 项目,需要添加 kotlinOptions,这是 Google 文档 Dependency injection with Hilt 中没有提到的,否则使用 ViewModel 会编译不过,下文会有详细的讲解。

Hilt 依赖添加方式相比于 Koin 太麻烦了,使用 koin 只需要添加相应的依赖就可以使用了。

Application 是 App 的入口,所以所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application

@HiltAndroidApp
class HiltApplication : Application() {
    /**
     * 1. 所有使用 Hilt 的 App 必须包含一个使用 @HiltAndroidApp 注解的 Application
     * 2. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖项容器的基类
     * 3. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖
     * 4. 在 Application 中设置好 @HiltAndroidApp 之后,就可以使用 Hilt 提供的组件了,
     *    Hilt 提供的 @AndroidEntryPoint 注解用于提供 Android 类的依赖(Activity、Fragment、View、Service、BroadcastReceiver)等等
     *    Application 使用 @HiltAndroidApp 注解
     */
}
  1. @HiltAndroidApp 将会触发 Hilt 代码的生成,包括用作应用程序依赖容器的基类
  2. 生成的 Hilt 组件依附于 Application 的生命周期,它也是 App 的父组件,提供其他组件访问的依赖

准备工作都做完了,接下来我们来看几个例子,如何使用 Hilt 进行依赖注入。

如何使用 Hilt 进行依赖注入

我们先来看一个简单的例子,注入 HiltSimple 并在 Application 中调用它的 doSomething 方法。

class HiltSimple @Inject constructor() {
    fun doSomething() {
        Log.e(TAG, "----doSomething----")
    }
}

@HiltAndroidApp
class HiltApplication : Application() {
    @Inject
    lateinit var mHiltSimple: HiltSimple

    override fun onCreate() {
        super.onCreate()
        mHiltSimple.doSomething()
    }
}

Hilt 需要知道如何从相应的组件中提供必要依赖的实例。使用 @Inject 注解来告诉 Hilt 如何提供该类的实例,@Inject 常用于构造函数、非私有字段、方法中。

Hilt 如何和 Android 组件一起使用

如果是 Hilt 支持的 Android 组件,直接使用 @AndroidEntryPoint 注解即可。

/**
 *
 * 为项目中的每个 Android 类生成一个 Hilt 组件,这些组件可以从它们各自的父类接收依赖项,
 * 如果是抽象类则不能使用 @AndroidEntryPoint 注解
 *
 * 如果使用 @AndroidEntryPoint 注解 Android 类,还必须注解依赖于它的 Android 类,
 * 例如 如果 注解 fragment 然后还必须注解  fragment 依赖的 Activity, 否则会抛出以下异常
 * java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
 */
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // 用到了 Fragment 1.2.0 中重要的更新
        // 可以查看之前写的这篇文章 @see https://juejin.cn/post/6844904167685750798
        supportFragmentManager.beginTransaction()
            .add(R.id.container, HiltFragment::class.java, null)
            .commit()
    }
}

/**
 *  如果 注解 fragment 然后还必须注解  fragment 依赖的 Activity, 否则会抛出以下异常
 * java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
 */
@AndroidEntryPoint
class HiltFragment : Fragment() {

    // 使用 @Inject 注解从组件中获取依赖
    @Inject
    lateinit var mHiltSimple: HiltSimple

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_hilt, container, false)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        mHiltSimple.doSomething()
    }
}
  • 如果是抽象类则不需要使用 @AndroidEntryPoint 注解。
  • @AndroidEntryPoint 注解 仅仅支持 ComponentActivity 的子类例如 FragmentActivity、AppCompatActivity 等等。
  • 如果使用 @AndroidEntryPoint 注解 Android 类,必须在它依赖的 Android 类添加同样的注解,例如在 Fragment 中添加 @AndroidEntryPoint 注解,必须在 Fragment 依赖的 Activity 上也添加 @AndroidEntryPoint 注解。

注意: 在 Activity 中添加 Fragment,用到了 Fragment 1.2.0 中重要的更新,可以查看之前写的这篇文章 [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析

Hilt 如何和第三方组件一起使用

如果要在项目中注入第三方依赖,我们需要使用 @Module 注解,使用 @Module注解的普通类,在其中创建第三方依赖的对象。

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object NetworkModule {

    /**
     * @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
     * @Singleton 提供单例
     */
    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideGitHubService(retrofit: Retrofit): GitHubService {
        return retrofit.create(GitHubService::class.java)
    }
}
  • @Module 常用于创建依赖类的对象(例如第三方库 OkHttp、Retrofit等等)。
  • 使用 @Module 注入的类,需要使用 @InstallIn 注解指定 module 的范围,会绑定到 Android 类对应的生命周期上。
  • @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。

Hilt 如何和 ViewModel 一起使用

在 App 模块中的 build.gradle 文件中添加以下代码。

implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'

注意: 这个是在 Google 文档上没有提到的,如果使用的是 kotlin 的话需要额外在 App 模块中的 build.gradle 文件中添加以下代码,否则调用 by viewModels() 会编译不过。

// For Kotlin projects
kotlinOptions {
    jvmTarget = "1.8"
}

在 ViewModel 对象的构造函数中使用 @ViewModelInject 注解提供一个 ViewModel。

class HiltViewModel @ViewModelInject constructor(
) : ViewModel() {

    /**
     * 在 LifeCycle 2.2.0 之后,可以用更精简的方法来完成,使用 LiveData 协程构造方法 (coroutine builder)。
     * liveData 协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit() 方法则用来更新 LiveData 的数据。
     *
     * 具体可以查看之前写的这篇文章 [https://juejin.cn/post/6844904193468137486#heading-10] 有详细介绍
     */
    val mHitLiveData = liveData {
        emit(" i am a ViewModelInject")
    }
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mHitViewModule: HiltViewModel by viewModels()
    
        override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        
        mHitViewModule.mHitLiveData.observe(this, Observer {
            tvResult.setText(it)
        })
    }
}

在 HiltViewModel 里面使用了 LifeCycle 2.2.0 之后新增的方法,LiveData 协程构造方法提供了一个协程代码块,产生的是一个不可变的 LiveData,emit() 方法则用来更新 LiveData 的数据,具体可以查看之前写的这篇文章 Jetpack 成员 Paging3 实践以及源码分析(一) 里面有详细介绍。

Hilt 如何和 Room 一起使用

这里需要用到 @Module 注解,使用 @Module 注解的普通类,在其中提供 Room 的实例。

@Module
@InstallIn(ApplicationComponent::class)
// 这里使用了 ApplicationComponent,因此 NetworkModule 绑定到 Application 的生命周期。
object RoomModule {

    /**
     * @Provides 常用于被 @Module 注解标记类的内部的方法,并提供依赖项对象。
     * @Singleton 提供单例
     */
    @Provides
    @Singleton
    fun provideAppDataBase(application: Application): AppDataBase {
        return Room
            .databaseBuilder(application, AppDataBase::class.java, "dhl.db")
            .fallbackToDestructiveMigration()
            .allowMainThreadQueries()
            .build()
    }

    @Provides
    @Singleton
    fun providePersonDao(appDatabase: AppDataBase): PersonDao {
        return appDatabase.personDao()
    }
}

总结

全文到这里就结束了,本篇文章的案例已经全部上传到了 GitHub:HiltSimple

这篇文章里面分别介绍了 @HiltAndroidApp、@AndroidEntryPoint、@Inject、@Module、@InstallIn、@Provides 的含义以及实战案例,下篇文章我们一起来分析一下 @EntryPoint,以及和其他 Jetpack 组件如何一起使用。

需要注意的是使用 Hilt 有三个需要注意的地方

  • 如果注解非 ComponentActivity 子类,例如 Activity 则会抛出以下异常。

    Activities annotated with @AndroidEntryPoint must be a subclass of androidx.activity.ComponentActivity. (e.g. FragmentActivity, AppCompatActivity, etc.)
    
  • 如果使用 @AndroidEntryPoint 注解 Android 类,必须在它依赖的 Android 类添加同样的注解,例如在 Fragment 中添加 @AndroidEntryPoint 注解,必须在 Fragment 依赖的 Activity 上也添加 @AndroidEntryPoint 注解 , 否则会抛出以下异常。

    java.lang.IllegalStateException: Hilt Fragments must be attached to an @AndroidEntryPoint Activity. Found: class com.hi.dhl.hilt.MainActivity
    
  • 需要注意的是如果同时使用 hilt 和 data binding,Android Studio 的版本必须 >= 4.0

    所以还没有升级的朋友们,尽快升级吧,升级到 Android Studio 4.0 也会遇到一些坑,不过好在这些坑现在都有相应的解决方案了。

Hilt 如果和 ViewModel 一起使用有点需要注意

这个是在 Google 文档上没有提到的,如果使用的是 kotlin 语言的话,需要额外在 App 模块中的 build.gradle 文件中添加以下代码,否则调用 by viewModels() 会编译不过。

kotlinOptions {
    jvmTarget = "1.8"
}

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,正在逐渐增加 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译、Jetpack 源码相关的文章,正在努力写出更好的文章,如果这篇文章对你有帮助给个 star,一起来学习,期待与你一起成长。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

Android 应用系列

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

工具系列