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

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

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

另外若是想了解 Google 新推出的另外两个 Jetpack 新成员 App StartupPaging3 的实践与原理,能够点击下方连接前去查看。git

经过这篇文章你将学习到如下内容:github

  • 什么是注解?面试

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

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

  • @Binds@Provides 的区别?编程

  • 限定符 @Qualifier 的使用?api

    • 自定义限定符 @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 class Generated component Scope
Application ApplicationComponent @Singleton
View Model ActivityRetainedComponent @ActivityRetainedScope
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @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,文章都会同步到这个仓库。

工具系列

相关文章
相关标签/搜索