10 Jetpack-Hilt-2

作者: 凤邪摩羯 | 来源:发表于2021-12-20 09:29 被阅读0次

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

    • 什么是注解?

    • @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 的方式在上一篇文章 都有介绍,这里我们来介绍如何使用注解 @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 等等

    image.png

    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 提供的依赖

    总结

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

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

    相关文章

      网友评论

        本文标题:10 Jetpack-Hilt-2

        本文链接:https://www.haomeiwen.com/subject/cwerultx.html