Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇

5,614 阅读14分钟

在上一篇文章 Jetpack 新成员 Hilt 实践(一)启程过坑记 分别介绍了 Hilt 的常用注解、以及在实践过程中遇到的一些坑,Hilt 如何 Android 框架类进行绑定,以及他们的生命周期,这篇文章继续讲解 Hilt 的用法,代码已经全部上传到 GitHub:HiltWithAppStartupSimple 如果对你有帮助,请在仓库右上角帮我点个赞。

Hilt 涉及的知识点有点多而且比较难理解,在看本篇文章之前一定要先看一下之前的文章 Jetpack 新成员 Hilt 实践(一),为了节省篇幅,这篇文章将会忽略 Hilt 环境配置的过程等等之前文章已经介绍过的内容。

另外如果想了解 Google 新推出的另外两个 Jetpack 新成员 App StartupPaging3 的实践与原理,可以点击下方链接前去查看。

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

  • 什么是注解?

  • @assist 注解和 SavedStateHandle 如何使用?

  • 如何使用 @Binds 注解实现接口注入?

  • @Binds@Provides 的区别?

  • 限定符 @Qualifier 的使用?

    • 自定义限定符 @qualifers
    • 预定义的限定符 @qualifers
  • 组件作用域 @scopes 如何使用?

  • 如何在 Hilt 不支持的类中执行依赖注入?

    • Hilt 如何和 ContentProvider 一起使用?
    • Hilt 如何和 App Startup 一起使用?

Hilt 是基于 Dagger 基础上进行开发的,如果了解 Dagger 朋友们,应该会感觉它们很像,但是与 Dagger 不同的是, Hilt 集成了 Jetpack 库和 Android 框架类,并删除了大部分模板代码,让开发者只需要关注如何进行绑定,而不需要管理所有 Dagger 配置的问题。

在上篇文章已经介绍过, Hilt 如何 Android 框架类进行绑定,以及他们的生命周期,这篇文章将介绍 Hilt 如何和 Jetpack 组件(ViewModel、App Startup)一起绑定,在开始介绍之前我们先来了解一下什么是注解。

什么是注解

之前有小伙伴在 WX 上问过我,对注解不太了解,所以想在这里想简单的提一下。

注解是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”,注解则可以被编译器打包进入 class 文件,可以在编译,类加载,运行时被读取。

常见的三个注解 @Override@Deprecated@SuppressWarnings

  • @Override: 确保子类重写了父类的方法,编译器会检查该方法是否正确地实现。
  • @Deprecated:表示某个类、方法已经过时,编译器会检查,如果使用了过时的方法,会给出提示。
  • @SuppressWarnings:编译器会忽略产生的警告。

Hilt 如何和 ViewModel 一起使用?

在上一篇文章只是简单的介绍了 Hilt 如何和 ViewModel 一起使用,我们继续介绍 ViewModel 的另外一个重要的参数 SavedStateHandle,首先需要添加依赖。

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

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

koltin 使用 kapt, Java 使用 annotationProcessor。

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

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

在 ViewModel 的构造函数中使用 @ViewModelInject 注解提供一个 ViewModel,如果需要用到 SavedStateHandle,需要使用 @assist 注解添加 SavedStateHandle 依赖项,代码如下所示。

class HiltViewModel @ViewModelInject constructor(
    private val tasksRepository: Repository,
    //SavedStateHandle 用于进程被终止时,保存和恢复数据
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    // getLiveData 方法会取得一个与 key 相关联的 MutableLiveData
    // 当与 key 相对应的 value 改变时 MutableLiveData 也会更新。
    private val _userId: MutableLiveData<String> = savedStateHandle.getLiveData(USER_KEY)

    // 对外暴露不可变的 LiveData
    val userId: LiveData<String> = _userId
    
    companion object {
        private val USER_KEY = "userId"
    }
}

将用户的 userId 存储在 SavedStateHandle 中,当进程被终止时保存和恢复对应的数据。

SavedStateHandle 是什么?SavedStateHandle 为了解决什么问题?

ActivityFragment 通常会在下面三种情况下被销毁(以下内容来自 Google):

  • 从当前界面永久离开: 用户导航至其他界面或直接关闭 Activity (通过点击返回按钮或执行的操作调用了 finish() 方法)。对应 Activity 实例被永久关闭。
  • Activity 配置 (configuration) 被改变: 例如旋转屏幕等操作,会使 Activity 需要立即重建。
  • 应用在后台时,其进程被系统杀死: 这种情况发生在设备剩余运行内存不足,系统又需要释放一些内存的时候,当进程在后台被杀死后,用户又返回该应用时 Activity 需要被重建。

ViewModel 会帮您处理第二种情况,因为在这种情况下 ViewModel 没有被销毁,而在第三种情况下,ViewModel 被销毁了, 当进程在后台被杀死后,则需要使用 onSaveInstanceState() 作为备用保存数据的方式。

SavedStateHandle 的出现就是为了解决 App 进程终止保存和恢复数据问题,ViewModel 不需要向 Activity 发送和接收状态。相反的,现在可以在 ViewModel 中处理保存和恢复数据。

SavedStateHandle 类似于一个 Bundle,它是数据的键-值映射,这个 SavedStateHandle 包含在 ViewModel 中,它在后台进程终止时仍然存在,以前保存在 onSaveInstanceState() 中的任何数据现在都可以保存在 SavedStateHandle 中。

使用 @Binds 注解实现接口注入?

注入接口实例有两种方式分别使用注解 @Binds@Provides@Provides 的方式在上一篇文章 Jetpack 新成员 Hilt 实践(一)启程过坑记 Hilt 如何和 Room 一起使用Hilt 如何和第三方组件一起使用 都有介绍,这里我们来介绍如何使用注解 @Binds

interface WorkService {
    fun init()
}

/**
 * 注入构造函数,因为 Hilt 需要知道如何提供 WorkServiceImpl 的实例
 */
class WorkServiceImpl @Inject constructor() :
    WorkService {

    override fun init() {
        Log.e(TAG, " I am an WorkServiceImpl")
    }
    
}

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

    /**
     * @Binds 注解告诉 Hilt 需要提供接口实例时使用哪个实现
     *
     * bindAnalyticsService 函数需要为 Hilt 提供了以下信息
     *      1. 函数返回类型告诉 Hilt 提供了哪个接口的实例
     *      2. 函数参数告诉 Hilt 提供哪个实现
     */
    @Binds
    abstract fun bindAnalyticsService(
        workServiceImpl: WorkServiceImpl
    ): WorkService
}

使用注解 @Binds 时,需要提供以下两个信息:

  • 函数参数告诉 Hilt 接口的实现类,例如参数 WorkServiceImpl 是接口 WorkService 的实现类。
  • 函数返回类型告诉 Hilt 提供了哪个接口的实例。

注解 @Binds 和 注解 @Provides 的区别?

  • @Binds:需要在方法参数里面明确指明接口的实现类。
  • @Provides:不需要在方法参数里面明确指明接口的实现类,由第三方框架实现,通常用于和第三方框架进行绑定(RetrofitRoom 等等)
// 有自己的接口实现
@Binds
abstract fun bindAnalyticsService(
    workServiceImpl: WorkServiceImpl
): WorkService

// 没有自己的接口实现
@Provides
fun providePersonDao(application: Application): PersonDao {
    return  Room
        .databaseBuilder(application, AppDataBase::class.java, "dhl.db")
        .fallbackToDestructiveMigration()
        .allowMainThreadQueries()
        .build().personDao()
}
        
@Provides
fun provideGitHubService(): GitHubService {
    return  Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build().create(GitHubService::class.java)
}

限定符 @Qualifier 注解的使用

来自 Google@Qualifier 是一种注解,当类型定义了多个绑定时,使用它来标识该类型的特定绑定。

换句话说 @Qualifier 声明同一个类型,可以在多处进行绑定,我将限定符分为两种。

  1. 自定义限定符
  2. 预定义限定符

自定义限定符的使用

我们先用注解 @Qualifier 声明两个不同的实现。

// 为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用
@Qualifier
// @Retention 定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)
@Retention(AnnotationRetention.BINARY)
annotation class RemoteTasksDataSource // 注解的名字,后面直接使用它

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class LocalTasksDataSource
  • @Qualifier :为每个声明的限定符,提供对应的类型实例,和 @Binds 或者 @Provides 一起使用

  • @Retention:定义了注解的生命周期,对应三个值(SOURCE、BINARY、RUNTIME)

    • AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中。
    • AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见。
    • AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见。

通常我们自定义的注解都是 RUNTIME,所以务必要加上@Retention(RetentionPolicy.RUNTIME) 这个注解

来看一下 @Qualifier@Provides 一起使用的例子,定义了两个方法,具有相同的返回类型,但是实现不同,限定符将它们标记为两个不同的绑定。

@Singleton
@RemoteTasksDataSource
@Provides
fun provideTasksRemoteDataSource(): DataSource { // 返回值相同
    return RemoteDataSource() // 不同的实现
}

@Singleton
@LocalTasksDataSource
@Provides
fun provideTaskLocalDataSource(appDatabase: AppDataBase): DataSource { // 返回值相同
    return LocalDataSource(appDatabase.personDao()) // 不同的实现
}

当我们声明完 @Qualifier 注解之后,就可以使用声明的两个 @Qualifier,来看个例子,定义一个 Repository 构造方法里面传入用 @Qualifier 注解声明的两个不同实现。

@Singleton
@Provides
fun provideTasksRepository(
    @LocalTasksDataSource localDataSource: DataSource,
    @RemoteTasksDataSource remoteDataSource: DataSource
): Repository {
    return TasksRepository(
        localDataSource,
        remoteDataSource
    )
}

provideTasksRepository 方法内,传入的参数都是 DataSource,但是前面用 @Qualifier 注解声明了它们不同的实现。

预定义限定符

Hilt 提供了一些预定义限定符,例如你可能在不同的情况下需要不同的 ContextApplictionActivity)Hilt 提供了 @ApplicationContext@ActivityContext 两种限定符。

class HiltViewModel @ViewModelInject constructor(
    @ApplicationContext appContext: Context,
    @ActivityContext actContext: Context,
    private val tasksRepository: Repository,
    @Assisted private val savedStateHandle: SavedStateHandle
)

组件作用域 @scopes 的使用

默认情况下,Hilt 中的所有绑定都是无作用域的,这意味着每次应用程序请求绑定时,Hilt 都会创建一个所需类型的新实例。

@scopes 的作用在指定作用域范围内(ApplicationActivity 等等) 提供相同的实例。

Hilt 还允许将绑定的作用域限定到特定组件,Hilt 只为绑定作用域到的组件的每个实例创建一次范围绑定,所有绑定请求共享同一个实例,我们来看一例子。

@Singleton
class HiltSimple @Inject constructor() {
}

HiltSimple@Singleton 声明了其作用域,那么在 Application 范围内提供相同的实例,代码如下所示,大家可以运行 Demo 看一下输出结果。

MainActivity: com.hi.dhl.hilt.appstartup.di.HiltSimple@8f75417
HitAppCompatActivity: com.hi.dhl.hilt.appstartup.di.HiltSimple@8f75417

注意:绑定组件范围可能非常的昂贵,因为提供的对象会保留在内存中,直到该组件被销毁,应该尽量减少在应用程序中使用绑定组件范围,对于要求在一定范围内使用同一实例的绑定,或者对于创建成本高昂的绑定,使用组件范围的绑定是合适的。

下表列出了每个生成组件的 scope 注解对应的范围。

Android classGenerated componentScope
ApplicationApplicationComponent@Singleton
View ModelActivityRetainedComponent@ActivityRetainedScope
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
View annotated with @WithFragmentBindingsViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped

在 Hilt 不支持的类中执行依赖注入

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

@EntryPoint:可以使用 @EntryPoint 注解创建入口点,@EntryPoint 允许 Hilt 使用 Hilt 无法在依赖中提供依赖的对象。

例如 Hilt 不支持 ContentProvider,如果你在想在 ContentProvider 中获取 Hilt 提供的依赖,你可以定义一个接口,并添加 @EntryPoint 注解,然后添加 @InstallIn 注解指定 module 的范围,代码如下所示。

@EntryPoint
@InstallIn(ApplicationComponent::class)
interface InitializerEntryPoint {

    fun injectWorkService(): WorkService

    companion object {
        fun resolve(context: Context): InitializerEntryPoint {

            val appContext = context.applicationContext ?: throw IllegalStateException()
            return EntryPointAccessors.fromApplication(
                appContext,
                InitializerEntryPoint::class.java
            )
        }
    }
}

使用 EntryPointAccessors 提供四个静态方法进行访问,分别是 fromActivityfromApplicationfromFragmentfromView 等等

EntryPointAccessors 提供四个静态方法,第一个参数是 @EntryPoint 接口上 @InstallIn 注解指定 module 的范围,我们在接口 InitializerEntryPoint@InstallIn 注解指定 module 的范围是 ApplicationComponent,所以我们应该使用 EntryPointAccessors 提供的静态方法 fromApplication

class WorkContentProvider : ContentProvider() {

    override fun onCreate(): Boolean {
        context?.run {
            val service = InitializerEntryPoint.resolve(this).injectWorkService()
            Log.e(TAG, "WorkContentProvider ${service.init()}")
        }
        return true
    }
    ......
}

ContentProvider 中调用 EntryPointAccessors 类中的 fromApplication 方法就可以获取到 Hit 提供的依赖。

Hilt 如何和 App Startup 一起使用

App Startup 会默认提供一个 InitializationProviderInitializationProvider 继承 ContentProvider,那么 Hilt 在 App Startup 中使用的方式和 ContentProvider 一样。

class AppInitializer : Initializer<Unit> {


    override fun create(context: Context): Unit {
        val service = InitializerEntryPoint.resolve(context).injectWorkService()
        Log.e(TAG, "AppInitializer ${service.init()}")
        return Unit
    }

    override fun dependencies(): MutableList<Class<out Initializer<*>>> =
        mutableListOf()

}

通过调用 EntryPointAccessors 的静态方法,获取到 Hit 提供的依赖,关于 App Startup 如何使用可以查看这篇文章 Jetpack 最新成员 AndroidX App Startup 实践以及原理分析

总结

到这里关于 Hilt 的注解使用都介绍完了,代码已经全部上传到了 GitHub:HiltWithAppStartupSimple

HiltWithAppStartupSimple 包含了本篇文章和 Jetpack 新成员 Hilt 实践(一)启程过坑记 文章中使用的案例,如果之前没有看过可以先去了解一下,之后看代码会更加的清楚。

Hilt 是基于 Dagger 基础上进行开发的,入门要比 Dagger 简单很多,不需要去管理所有的 Dagger 的配置问题,但是其入门的门槛还是很高的,尤其是 Hilt 的注解,需要了解其每个注解的含义才能正确的使用,避免资源的浪费。

这篇文章和之前 Jetpack 新成员 Hilt 实践(一)启程过坑记 的文章其中很多案例都重新去设计了,因为 Google 的提供的案例,确实很难让人理解,希望这两篇文章可以帮助小伙伴们快速入门 Hilt,后面还会有更多实战案例。

计划建立一个最全、最新的 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,文章都会同步到这个仓库。

工具系列