美文网首页Android Jetpack 系列Jetpackkotlin
Android Jetpack系列--9. Hilt使用详解

Android Jetpack系列--9. Hilt使用详解

作者: 今阳说 | 来源:发表于2021-09-14 17:03 被阅读0次

    相关知识

    依赖注入

    • Dependency Injection,简称DI;
    • 依赖项注入可以使代码解耦,便于复用,重构和测试

    什么是依赖项注入

    • 类通常需要引用其他类,可通过以下三种方式获取所需的对象:
    1. 在类中创建所需依赖项的实例
    class CPU () {
        var name: String = ""
        fun run() {
            LjyLogUtil.d("$name run...")
        }
    }
    class Phone1 {
        val cpu = CPU()
        fun use() {
            cpu.run()
        }
    }
    
    1. 通过父类或其他类获取
    val cm = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
    val networkCapabilities = cm.getNetworkCapabilities(cm.activeNetwork)
    LjyLogUtil.d("是否有网络连接:${networkCapabilities==null}")
    
    1. 以参数形式提供,可以在构造类时提供这些依赖项,或者将这些依赖项传入需要各个依赖项的函数;
    • 第三种方式就是依赖项注入

    Android中的依赖注入方式

    手动依赖注入
    1. 构造函数注入
    //在构造类时提供这些依赖项
    class Phone (private val cpu: CPU) {
        fun use() {
            cpu.run()
        }
    }
    val cpu = CPU()
    val phone=Phone(cpu)
    phone.use()
    
    2. 字段注入(或 setter 注入)
    • 依赖项将在创建类后实例化
    //将依赖项传入需要依赖项的函数
    class Phone {
        lateinit var cpu: CPU
        fun use() = cpu.run()
    }
    val phone = Phone()
    val cpu = CPU()
    phone.cpu = cpu
    phone.use()
    
    • 上面两种都是手动依赖项注入,但是如果依赖项和类过多,手动依赖注入就会产生一些问题
    1. 使用越来越繁琐;
    2. 产生大量模板代码;
    3. 必须按顺序声明依赖项;
    4. 很难重复使用对象;
    
    自动依赖注入框架
    • 有一些库通过自动执行创建和提供依赖项的过程解决此问题,实现原理有如下几种方案:
    1. 通过反射,在运行时连接依赖项;
    2. 通过注解,编译时生成连接依赖项的代码;
    3. kotlin 强大的语法糖和函数式编程;
    
    1. Dagger:
    • Android领域最广为熟知的依赖注入框架,可以说大名鼎鼎了
    Dagger 1.x版本:Square基于反射实现的,有两个缺点一个是反射的耗时,另一个是反射是运行时的,编译期不会报错。而使用难度较高,刚接触时有经常容易写错,造成开发效率底;
    Dagger 2.x版本:Google基于Java注解实现的,完美解决了上述问题,
    
    2. Koin
    • 为 Kotlin 开发者提供的一个实用型轻量级依赖注入框架,采用纯 Kotlin 语言编写而成,仅使用功能解析,无代理、无代码生成、无反射(通过kotlin 强大的语法糖(例如 Inline、Reified 等等)和函数式编程实现);
    3. Hilt:
    • 由于Dagger的复杂度和使用难度较大,Android团队联合Dagger2团队,一起开发出来的一个专门面向Android的依赖注入框架Hilt,最明显的特征就是:1. 简单;2. 提供了Android专属的API;3. Google官方支持,和Jetpack其他组件配合使用;

    Hilt

    • Hilt 通过为项目中的每个 Android 类提供容器并自动为您管理其生命周期,定义了一种在应用中执行 DI 的标准方法。
    • Hilt 在热门 DI 库 Dagger 的基础上构建而成,因而能够受益于 Dagger 提供的编译时正确性、运行时性能、可伸缩性和 Android Studio 支持。

    Hilt使用流程

    添加依赖项

    //1. 配置Hilt的插件路径
    buildscript {
        ...
        dependencies {
            ...
            classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
        }
    }
    //2. 引入Hilt的插件
    plugins {
        ...
        id 'dagger.hilt.android.plugin'
        id 'kotlin-kapt'
    }
    //3. 添加Hilt的依赖库及Java8
    android {
      ...
      compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
      }
      //对于 Kotlin 项目,需要添加 kotlinOptions
      kotlinOptions {
          jvmTarget = "1.8"
      }
    }
    dependencies {
        ...
        implementation "com.google.dagger:hilt-android:2.28-alpha"
        kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"
    }
    

    Hilt 应用类

    • 用@HiltAndroidApp注解Application;
    • @HiltAndroidApp注解 会触发 Hilt 的代码生成操作,生成的代码包括应用的一个基类,该基类充当应用级依赖项容器;
    @HiltAndroidApp
    class MyApplication : MultiDexApplication() {
        ...
    }
    

    将依赖项注入 Android 类

    1. 用@AndroidEntryPoint注释类;
    • 目前支持6类入口点:Application(通过使用 @HiltAndroidApp),Activity,Fragment,View,Service,BroadcastReceiver
    • 使用 @AndroidEntryPoint 注解 Android 类,还必须为依赖于该类的 Android 类添加注释,例如为注解 fragment ,则还必须为该 fragment 依赖的 Activity 添加@AndroidEntryPoint注释。
    2. 使用 @Inject 注释执行字段
    • @AndroidEntryPoint 会为项目中的每个 Android 类生成一个单独的 Hilt 组件。这些组件可以从它们各自的父类接收依赖项, 如需从组件获取依赖项,请使用 @Inject 注释执行字段注入, 注意:Hilt注入的字段是不可以声明成private的;
    3. 构造函数中使用 @Inject 注释
    • 为了执行字段注入,需要在类的构造函数中使用 @Inject 注释,以告知 Hilt 如何提供该类的实例:
    @AndroidEntryPoint
    class HiltDemoActivity : AppCompatActivity() {
        @Inject
        lateinit var cpu: CPU
        ...
    }
    
    class CPU @Inject constructor() {
        var name: String = ""
    
        fun run() {
            LjyLogUtil.d("$name run...")
        }
    }
    
    带参数的依赖注入:
    • 如果构造函数中带有参数,Hilt要如何进行依赖注入呢?
    • 需要构造函数中所依赖的所有其他对象都支持依赖注入
    class CPU @Inject constructor() {
        var name: String = ""
        fun run() {
            LjyLogUtil.d("$name run...")
        }
    }
    
    class Phone @Inject constructor(val cpu: CPU) {
        fun use() {
            cpu.run()
        }
    }
    
    @AndroidEntryPoint
    class HiltActivity : AppCompatActivity() {
        @Inject
        lateinit var phone: Phone
    
        fun test() {
            phone.cpu.name = "麒麟990"
            phone.use()
        }
    }
    

    Hilt Module

    • 有时一些类型参数不能通过构造函数注入, 如 接口 或 来自外部库的类,此时可以使用 Hilt模块 向Hilt提供绑定信息;
    • Hilt 模块是一个带有 @Module 注释的类,并使用 @InstallIn 设置作用域
    使用 @Binds 注入接口实例
    //1. 接口
    interface ICPU {
        fun run()
    }
    //2. 实现类
    class KylinCPU @Inject constructor() : ICPU {
        override fun run() {
            LjyLogUtil.d("kylin run...")
        }
    }
    //3. 被注入的类,入参是接口类型
    class Phone @Inject constructor(val cpu: ICPU) {
        fun use() {
            cpu.run()
        }
    }
    //4. 使用@Binds注入接口实例
    @Module
    @InstallIn(ActivityComponent::class)
    abstract class CPUModel {
        @Binds
        abstract fun bindCPU(cpu: KylinCPU): ICPU
    }
    //5. 使用注入的实例
    @AndroidEntryPoint
    class HiltActivity : AppCompatActivity() {
        @Inject
        lateinit var phone: Phone
    
        fun test() {
            phone.cpu.name = "麒麟990"
            phone.use()
        }
    }
    
    使用 @Provides 注入实例
    • 如果某个类不归您所有(因为它来自外部库,如 Retrofit、OkHttpClient 或 Room 数据库等类),或者必须使用构建器模式创建实例,也无法通过构造函数注入。
    @Module
    @InstallIn(ApplicationComponent::class)
    class NetworkModel {
        @Provides
        fun provideOkHttpClient(): OkHttpClient {
            return OkHttpClient().newBuilder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(90, TimeUnit.SECONDS)
                .build()
        }
    }
    
    为同一类型提供多个绑定
    • 比如网络请求中可能需要不同配置的OkHttpClient,或者不同BaseUrl的Retrofit
    • 使用@Qualifier注解实现
    //1. 接口和实现类
    interface ICPU {
        fun run()
    }
    
    
    class KylinCPU @Inject constructor() : ICPU {
        override fun run() {
            LjyLogUtil.d("kylin run...")
        }
    }
    
    class SnapdragonCPU @Inject constructor() : ICPU {
        override fun run() {
            LjyLogUtil.d("snapdragon run...")
        }
    }
    
    //2. 创建多个类型的注解
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class BindKylinCPU
    
    @Qualifier
    @Retention(AnnotationRetention.BINARY)
    annotation class BindSnapdragonCPU
    
    //@Retention:注解的生命周期
    //AnnotationRetention.SOURCE:仅编译期,不存储在二进制输出中
    //AnnotationRetention.BINARY:存储在二进制输出中,但对反射不可见
    //AnnotationRetention.RUNTIME:存储在二进制输出中,对反射可见
    
    
    //3. 在Hilt模块中使用注解
    @Module
    @InstallIn(ActivityComponent::class)
    abstract class CPUModel {
        @BindKylinCPU
        @Binds
        abstract fun bindKylinCPU(cpu: KylinCPU): ICPU
    
        @BindSnapdragonCPU
        @Binds
        abstract fun bindSnapdragonCPU(cpu: SnapdragonCPU): ICPU
    }
    
    //4. 使用依赖注入获取实例,可以用在字段注解,也可以用在构造函数或者方法入参中
    class Phone5 @Inject constructor(@BindSnapdragonCPU private val cpu: ICPU) {
        @BindKylinCPU
        @Inject
        lateinit var cpu1: ICPU
    
        @BindSnapdragonCPU
        @Inject
        lateinit var cpu2: ICPU
    
        fun use() {
            cpu.run()
            cpu1.run()
            cpu2.run()
        }
    
        fun use(@BindKylinCPU cpu: ICPU) {
            cpu.run()
        }
    }
    

    组件默认绑定

    • 由于可能需要来自 Application 或 Activity 的 Context 类,因此 Hilt 提供了 @ApplicationContext 和 @ActivityContext 限定符。
    • 每个 Hilt 组件都附带一组默认绑定,Hilt 可以将其作为依赖项注入您自己的自定义绑定
    class Test1 @Inject constructor(@ApplicationContext private val context: Context)
    
    class Test2 @Inject constructor(@ActivityContext private val context: Context)
    
    • 对于Application和Activity这两个类型,Hilt也是给它们预置好了注入功能(必须是这两个,即使子类也不可以)
    class Test3 @Inject constructor(val application: Application)
    
    class Test4 @Inject constructor(val activity: Activity)
    

    Hilt内置组件类型

    • 上面使用Hilt Module时,有用到@InstallIn(), 意思是把这个模块安装到哪个组件中
    Hilt内置了7种组件可选:
    1. ApplicationComponent:对应Application,依赖注入实例可以在全项目中使用
    2. ActivityRetainedComponent:对应ViewModel(在配置更改后仍然存在,因此它在第一次调用 Activity#onCreate() 时创建,在最后一次调用 Activity#onDestroy() 时销毁)
    3. ActivityComponent:对应Activity,Activity中包含的Fragment和View也可以使用;
    4. FragmentComponent:对应Fragment
    5. ViewComponent:对应View
    6. ViewWithFragmentComponent:对应带有 @WithFragmentBindings 注释的 View
    7. ServiceComponent:对应Service
    • Hilt 没有为 broadcast receivers 提供组件,因为 Hilt 直接从 ApplicationComponent 注入 broadcast receivers;

    组件作用域

    • Hilt默认会为每次的依赖注入行为都创建不同的实例。
    Hilt内置7种组件作用域注解
    1. @Singleton:对应组件ApplicationComponent,整个项目共享同一个实例
    2. @ActivityRetainedScope:对应组件ActivityRetainedComponent
    3. @ActivityScoped:对应组件ActivityComponent,在同一个Activity(包括其包含的Fragment和View中)内部将会共享同一个实例
    4. @FragmentScoped:对应组件FragmentComponent
    5. @ViewScoped:对应组件ViewComponent和ViewWithFragmentComponent;
    6. @ServiceScopedService:对应ServiceComponent
    • 比如我们经常会需要一个全局的OkhttpClient或者Retrofit,就可以如下实现
    interface ApiService {
        @GET("search/repositories?sort=stars&q=Android")
        suspend fun searRepos(@Query("page") page: Int, @Query("per_page") perPage: Int): RepoResponse
    }
    
    @Module
    @InstallIn(ApplicationComponent::class)
    class NetworkModel {
    
        companion object {
            private const val BASE_URL = "https://api.github.com/"
        }
    
        @Singleton
        @Provides
        fun provideApiService(retrofit: Retrofit): ApiService {
            return retrofit.create(ApiService::class.java)
        }
    
        //组件作用域:Hilt默认会为每次的依赖注入行为都创建不同的实例。
        @Singleton
        @Provides
        fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
            return Retrofit.Builder()
                .addConverterFactory(GsonConverterFactory.create())
                .baseUrl(BASE_URL)
                .client(okHttpClient)
                .build()
        }
    
        @Singleton
        @Provides
        fun provideOkHttpClient(): OkHttpClient {
            return OkHttpClient().newBuilder()
                .connectTimeout(30, TimeUnit.SECONDS)
                .readTimeout(60, TimeUnit.SECONDS)
                .writeTimeout(90, TimeUnit.SECONDS)
                .build()
        }
    }
    
    • 或是使用Room操作本地数据库
    @Database(entities = [RepoEntity::class], version = Constants.DB_VERSION)
    abstract class AppDatabase : RoomDatabase() {
        abstract fun repoDao(): RepoDao
    }
    
    @Module
    @InstallIn(ApplicationComponent::class)
    object RoomModule {
        @Provides
        @Singleton
        fun provideAppDatabase(application: Application): AppDatabase {
            return Room
                .databaseBuilder(
                    application.applicationContext,
                    AppDatabase::class.java,
                    Constants.DB_NAME
                )
                .allowMainThreadQueries() //允许在主线程中查询
                .build()
        }
    
        @Provides
        @Singleton
        fun provideRepoDao(appDatabase: AppDatabase):RepoDao{
            return appDatabase.repoDao()
        }
    }
    

    ViewModel的依赖注入

    • 通过上面的学习,那我们如果在ViewModel中创建Repository要如何实现呢,可以如下:
    //1. 仓库层
    class Repository @Inject constructor(){
        @Inject
        lateinit var apiService: ApiService
        suspend fun getData(): RepoResponse {
            return apiService.searRepos(1, 5)
        }
    }
    //2. ViewModel层
    @ActivityRetainedScoped
    class MyViewModel @Inject constructor(private val repository: Repository): ViewModel() {
        var result: MutableLiveData<String> = MutableLiveData()
        fun doWork() {
            viewModelScope.launch {
                runCatching {
                    withContext(Dispatchers.IO){
                        repository.getData()
                    }
                }.onSuccess {
                    result.value="RepoResponse=${gson().toJson(it)}"
                }.onFailure {
                    result.value=it.message
                }
            }
        }
    }
    //3. Activity层
    @AndroidEntryPoint
    class HiltMvvmActivity : AppCompatActivity() {
    
        @Inject
        lateinit var viewModel: MyViewModel
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_hilt_mvvm)
            viewModel.result.observe(this, Observer {
                LjyLogUtil.d("result:$it")
            })
            lifecycleScope
            viewModel.doWork()
        }
    }
    
    ViewModel 和 @ViewModelInject 注解
    • 这种改变了获取ViewModel实例的常规方式, 为此Hilt专门为其提供了一种独立的依赖注入方式: @ViewModelInject
    //1. 添加两个额外的依赖
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha02'
    kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha02'
    //2. 修改MyViewModel: 去掉@ActivityRetainedScoped注解,把@Inject改为@ViewModelInject
    class MyViewModel @ViewModelInject constructor(private val repository: Repository): ViewModel() {
        ...
    }
    //3. activity中viewModel获取改为常规写法
    @AndroidEntryPoint
    class HiltMvvmActivity : AppCompatActivity() {
    //    @Inject
    //    lateinit var viewModel: MyViewModel
        val viewModel: MyViewModel by viewModels()
    //  或  val viewModel: MyViewModel by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
        ...
    }
    
    SavedStateHandle 和 @assist注解
    • Activity/Fragment被销毁一般有三种情况:
      1. 界面关闭或退出应用
      2. Activity 配置 (configuration) 被改变,如旋转屏幕时;
      3. 在后台时因运行内存不足被系统回收;
    • ViewModel 会处理2的情况,而3的情况就需要使用onSaveInstanceState()保存数据,重建时用SavedStateHandle恢复数据,就要用@assist 注解添加 SavedStateHandle 依赖项
    class MyViewModel @ViewModelInject constructor(
        private val repository: Repository,
        //SavedStateHandle 用于进程被终止时,保存和恢复数据
        @Assisted private val savedStateHandle: SavedStateHandle
    ) : ViewModel() {
        var result: MutableLiveData<String> = MutableLiveData()
        private val userId: MutableLiveData<String> = savedStateHandle.getLiveData("userId")
    
        fun doWork() {
            viewModelScope.launch {
                runCatching {
                    withContext(Dispatchers.IO) {
                        repository.getData(userId)
                    }
                }.onSuccess {
                    result.value = "RepoResponse=${Gson().toJson(it)}"
                }.onFailure {
                    result.value = it.message
                }
            }
        }
    }
    

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

    • 可以使用 @EntryPoint 注释创建入口点, 调用EntryPointAccessors的静态方法来获得自定义入口点的实例
    • EntryPointAccessors提供了四个静态方法:fromActivity、fromApplication、fromFragment、fromView,根据自定义入口的MyEntryPoint的注解@InstallIn所指定的范围选择对应的获取方法;
    @EntryPoint
    @InstallIn(ApplicationComponent::class)
    interface MyEntryPoint{
        fun getRetrofit():Retrofit
    }
    
    在ContentProvider中使用
    • Hilt支持的入口点中少了一个关键的Android组件:ContentProvider, 主要原因就是ContentProvider.onCreate() 在Application的onCreate() 之前执行,因此很多人会利用这个特性去进行提前初始化, 详见Android Jetpack系列--5. App Startup使用详解, 而Hilt的工作原理是从Application.onCreate()中开始的,即ContentProvider.onCreate()执行之前,Hilt的所有功能都还无法正常工作;
    class MyContentProvider : ContentProvider() {
        override fun onCreate(): Boolean {
            context?.let {
                val appContext=it.applicationContext
                //调用EntryPointAccessors.fromApplication()函数来获得自定义入口点的实例
                val entryPoint=EntryPointAccessors.fromApplication(appContext,MyEntryPoint::class.java)
                //再调用入口点中定义的getRetrofit()函数就能得到Retrofit的实例
                val retrofit=entryPoint.getRetrofit()
                LjyLogUtil.d("retrofit:$retrofit")
            }
            return true
        }
        ...
    }
    
    在 App Startup 中使用
    • App Startup 会默认提供一个 InitializationProvider,InitializationProvider 继承 ContentProvider;
    class LjyInitializer : Initializer<Unit> {
        override fun create(context: Context) {
            //调用EntryPointAccessors.fromApplication()函数来获得自定义入口点的实例
            val entryPoint= EntryPointAccessors.fromApplication(context, MyEntryPoint::class.java)
            //再调用入口点中定义的getRetrofit()函数就能得到Retrofit的实例
            val retrofit=entryPoint.getRetrofit()
            LjyLogUtil.d("retrofit:$retrofit")
        }
    
        override fun dependencies(): List<Class<out Initializer<*>>> {
            return emptyList()
        }
    }
    

    报错解决

    • Expected @HiltAndroidApp to have a value. Did you forget to apply the Gradle Plugin?
    android {
        ...
        defaultConfig {
            ...
            javaCompileOptions {
                annotationProcessorOptions {
                    arguments = ["room.schemaLocation":
                                 "$projectDir/schemas".toString()]
                }
            }
        }
    }
    //If so, try changing from "arguments =" to "arguments +=", as just using equals overwrites anything set previously.
    

    我是今阳,如果想要进阶和了解更多的干货,欢迎关注微信公众号 “今阳说” 接收我的最新文章

    相关文章

      网友评论

        本文标题:Android Jetpack系列--9. Hilt使用详解

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