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

相关知识

依赖注入

  • 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.

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容