Jetpack系列(八) -- Hilt依赖注入

1,457 阅读6分钟

前言

时间: 23/10/02

AndroidStudio版本: Giraffe 2022.3.1 JDK:17 开发语言: Kotlin

Gradle版本: 8.0 Gradle plugin Version: 8.1.1

概述

  • 什么是依赖注入?

    简单来说就是在一个类中使用到的依赖类不是这使用者类本身创建的,而是使用构造函数或者属性方法getter得到的,这种实现的方式就叫依赖注入。

    举个简单的例子

    //假设有一个 TeamManager 类,这个类是管理Team这个实体类的
    class TeamManager constructor() {
    
        fun getTeam() : Team {
            return Team(7, "DreamFire", "心中有梦, 肚里有火")
        }
    }
    //实体类 Team
    class Team(var id: Int, var name: String, var slogan: String) {
        override fun toString(): String {
            return "Team Name: $name\nSlogan: $slogan"
        }
    }
    

    那么在其他类中使用 Manager 时就需要通过构造函数实例化这个 Manager 类。例如在MainActivity中使用

    class MainActivity : AppCompatActivity() {
    
        private lateinit var binding: ActivityMainBinding
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
            
            val teamManager = TeamManager()//构造函数
            binding.btnGetInfo.setOnClickListener {
                val team = teamManager.getTeam()//使用方法
                binding.team = team
            }
    
        }
    }
    

    我们在MainActivity中使用了依赖类 TeamManager,通过构造函数实现实例化来使用依赖类中的方法,这就是最简单的依赖注入。

    本节就依赖注入问题讲述一下 Hilt 的使用和基础原理。

Hilt的使用

  • 依赖包引入

    project层 build.gradle.kts

        id("com.google.dagger.hilt.android") version "2.46" apply false
    

    module层

    plugins {
        id("com.android.application")
        id("org.jetbrains.kotlin.android")
        id("kotlin-kapt")
        id("com.google.dagger.hilt.android")
    }
    
    dependencies {
    
    	....
    
        implementation("com.google.dagger:hilt-android:2.46")
        kapt("com.google.dagger:hilt-android-compiler:2.46")
    }
    
    kapt {
        correctErrorTypes = true
    }
    

    由于Google官方文档中版本是 2.44 这和 1.9.0 版本的 kotlin 并不适配,并且我一直没找到版本对应的文档,所以我是一个一个添加往上试的。如果开发过程中出现依赖问题,可以尝试更改 Hilt 的版本。

  • Hilt 当前支持的 Android 类及其注解与注意事项

    Android 类注解注意事项
    Application@HiltAndroidApp必须定义一个Application,并添加注解
    Activity@AndroidEntryPoint仅支持扩展ComponentActivity的Activity
    Fragment@AndroidEntryPoint仅支持扩展androidx.Fragment的Fragment
    View@AndroidEntryPoint/
    Service@AndroidEntryPoint/
    BroadcastReceiver@AndroidEntryPoint/

普通对象依赖注入

  • 新建一个类继承application(),并添加@HiltAndroidApp注解

    @HiltAndroidApp
    class App : Application() 
    

    记得在AndroidManifest中添加name。

    ...
    <application
            android:name=".App"
            ...
    >
    
  • 修改TeamManager,给构造函数添加 @Inject 注解

    class TeamManager @Inject constructor() {
    
        fun getTeam() : Team {
            return Team(7, "DreamFire", "心中有梦, 肚里有火")
        }
    }
    
  • 修改 MainActivity 中 TeamManager 的构造过程,并给 Activity 添加 @AndroidEntryPoint 注解

    @AndroidEntryPoint
    class MainActivity : AppCompatActivity() {
        @Inject lateinit var teamManager: TeamManager//使用@Inject构造方法
    
        private lateinit var binding: ActivityMainBinding
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            binding = ActivityMainBinding.inflate(layoutInflater)
            setContentView(binding.root)
    
            binding.btnGetInfo.setOnClickListener {
                val team = teamManager.getTeam()//直接调用TeamManager中的方法
                binding.team = team
            }
        }
    }
    

这就是普通对象的依赖注入方法。

补充一点,这里我使用了数据绑定,textview中text属性设置为 android:text="@{team.toString()}",代码编写过程中可以自行操作

对象嵌套对象的依赖注入

假设我们在使用 TeamManager 的一个方法时,这个方法里有其它类的依赖,那如何实现呢?答案也很简单,全部添加注解,统统注解。举个例子

  • 添加一个类 ALGS,实现一个 isALGSTeam 方法

    class ALGS @Inject constructor() {
    
        fun isALGSTeam(): Boolean {
            Log.d("ALGS", "isALGSTeam: true")
            return true
        }
    }
    
  • 修改一个 TeamManager 的构造方法,添加一个方法

    class TeamManager @Inject constructor(val algs: ALGS) {
    	...
    	
        fun isALGSTeam(): Boolean = algs.isALGSTeam()
    }
    
  • 在 MainActivity 中调用即可

            binding.btnGetInfo.setOnClickListener {
    
                binding.tvShow2.text = "Is ALGS Team: ${teamManager.isALGSTeam()}"
            }
    

在第三方组件中使用依赖注入

上节讲述 Kotlin 协程时,用到了 Retrofit 框架,那这里也用 Kotlin 框架为示例,看看如何在 Retrofit 中使用依赖注入。

Retrofit 依赖包引入

    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

创建一个 ApiService 类

interface ApiService {
    @GET("repositories") //网络请求示例
    suspend fun getAllRepositories(): List<Repositories>

    @GET("users")
    suspend fun getAllUsers(): List<User>
}

一般来说我们对于网络请求包括 Retrofit 的构建都需要在一个类中完成,而不能在 Activity 中需要使用时临时创建。因此我们需要创建一个object类 NetworkUtil

@Module
@InstallIn(SingletonComponent::class)
//@InstallIn(ActivityComponent::class)
object NetworkUtil {

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Singleton
    @Provides
    fun teamApiService(): ApiService {
        return retrofit.create()
    }
}

添加@Module@InstallIn注解,其中 @InstallIn 注解需要传入一个参数,参数对应注入场景,一般来说 NetworkUtil 对象需要设置成单例模式,所以是 object 类。所以对应的注解参数为SingletonComponent::class

@Module注解表示这是一个用于提供依赖注入的模块。@InstallIn注解表示要装到哪个模块中,即注入场景

---- start modify ----

从常规角度来讲,使用了 object 类就不需要搞依赖注入了,因为它本身就是一个静态类,方法也是静态方法,其它类是可以直接调用的。这里其实可以使用 class 来替代 object。因为 object 静态的缘故,相对于 class 来说,代码的性能会受到一定的影响

这里就不作修改了

---- modify at 10/03 ----

对应关系表:

组件名称注入器面向的对象生命周期
SingletonComponentApplicationonCreate~onDestory
ViewModelComponentViewModelonCreate~onDestory
ActivityComponentActivityonCreate~onDestory
FragmentComponentFragmentonAttach~onDestory
ViewComponentViewView#super()~view销毁
ViewWithFragmentComponent@WithFragmentBindings注解的ViewView#super()~view销毁
ServiceComponentServiceonCreate~onDestory

@Singleton注解是Application组件类的作用域,Hilt 只为绑定的作用域中的组件实例创建一次作用于绑定,并对该绑定的所有请求共享同一实例

@Provides注解提供获取方法,方法名随意,类型为方法返回的类型

对应关系表:

Android 类组件名称作用域
ApplicationSingletonComponent@Singleton
ActivityActivityRetainedComponent@ActivityRetainedScoped
ViewModelViewModelComponent@ViewModelScoped
ActivityActivityComponent@ActivityScoped
FragmentFragmentComponent@FragmentScoped
ViewViewComponent@ViewScoped
@WithFragmentBindings注解的ViewViewWithFragmentComponent@ViewScoped
ServiceServiceComponent@ServiceScoped

在 MainActivity中添加使用

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    private val TAG = "MainActivity"
    @Inject lateinit var teamManager: TeamManager
    @Inject lateinit var teamApiService: ApiService

    private lateinit var binding: ActivityMainBinding
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        val job = Job()
        binding.btnGetInfo.setOnClickListener {
            val team = teamManager.getTeam()
            binding.team = team

            CoroutineScope(job).launch(Dispatchers.Main) {
                binding.tvShow2.text = "please wait, loading..."
                val result = withContext(Dispatchers.IO) {
                    try {
                        return@withContext teamApiService.getAllUsers()[0].toString()
                    } catch (e: IOException) {
                        Log.e(TAG, "onCreate: ", )
                        return@withContext "network error"
                    }
                }
                binding.tvShow2.text = result
            }
        }
    }
}

运行,点击按钮。闪退了,因为忘记添加联网权限。

<uses-permission android:name="android.permission.INTERNET" />

再次运行完美

part8_result1

@Provides方法重复问题

根据上述描述可知,@Provides 注解的方法在 @Inject 注入使用时,是通过方法的返回类型来确定需要注入的实例的。也就是说如果我们在同一个作用域中有两个不同的方法返回了同一种类型,即有两个注入同一实例的方法,那么 @Provides 注解就不知道提供哪个方法了。

例子,添加一个

object NetworkUtil {

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.github.com/")
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    @Singleton
    @Provides
    fun teamApiService(): ApiService {
        return retrofit.create()
    }

    @Singleton
    @Provides
    fun repoApiService(): ApiService {
        val retrofit = Retrofit.Builder()
            //实际开发过程中, 可能baseurl并不只有一个,这里只做示范
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        return retrofit.create()
    }
}

编译报错:

image-20231002185935457

那怎么解决这个问题呢?

报错的原因是无法区分两个实例,那就想办法区分出来。参考Google官方文档

新建一个 QualifierConfig.kt 文件

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GitHubApi

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class GitHubApi2

自定义两个 annotation 注解类,类名随意有意义就行,给两个方法区分开来。

    @Singleton
    @Provides
    @GitHubApi//对应 annotation class
    fun teamApiService(): ApiService {
        return retrofit.create()
    }

    @Singleton
    @Provides
    @GitHubApi2//对应 annotation class
    fun repoApiService(): ApiService {
        val retrofit = Retrofit.Builder()
            //实际开发过程中, 可能baseurl并不只有一个,这里只做示范
            .baseUrl("https://api.github.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
        return retrofit.create()
    }

然后在 MainActivity 中更改注入的代码

@Inject @GitHubApi2 lateinit var teamApiService: ApiService

这样就能正常运行了。

总结

本节主要讲述 Hilt 的使用,包括常规情况的使用和三方框架的使用,使用 Hilt 结合kotlin协程可以大大减少网络请求的代码量。

当然 Hilt 在和 jetpack 结合使用的情况还有很多,后续再做细聊。包括前面讲的Koltin协程,Room,甚至DataBinding 我都有一些细致的东西没有讲述到,这些遗漏的东西在正常使用或了解简单用法时可能都不太会涉及,后续进行App实战开发时再提。

Demo地址(GitHub)