Kotlin 40. Dependency Injection 依赖注入以及Hilt在Kotlin中的使用,系列3:Hilt 注释介绍及使用案例

一起来学Kotlin:概念:27. Dependency Injection 依赖注入以及Hilt在Kotlin中的使用,系列3:Hilt 注释介绍及使用案例

此系列博客中,我们将主要介绍:

  • Dependency Injection(依赖注入) 概念介绍。网上看了许多关于 DI 的介绍,云里雾里。这里,我们通过通俗易懂地方式对其进行介绍。
  • 手动依赖注入介绍。为了让大家更容易理解 Hilt,我们先介绍如何通过手动的方式实现依赖注入效果。
  • Hilt 注释(annotations)介绍及使用案例
  • MVVM 案例中如何使用 Hilt

此博客基于一个非常简单的 Kotlin 项目来解释 Hilt 的使用方式。



前言

1 回顾

在系列的第一篇博客中,我们介绍了依赖注入的概念,以及为什么需要依赖注入。

在系列的第二篇博客中,我们介绍了手动依赖注入。我们并不建议在项目中使用手动依赖注入,但我们可以通过手动依赖注入的介绍,来解释Hilt主要完成的两件事:

  • 提供了“containers”(容器)用来装各种依赖;
  • 自动管理这些“containers”(容器)的“lifecycles”(生命周期)。

在下面的章节中,我们主要解决一个问题:Hilt 具体应该在项目中怎么使用。

2 Hilt 的相关注释(annotations)

我们先对 Hilt 涉及到的注释进行一一介绍,这些注释包括:

  • @HiltAndroidApp
  • @AndroidEntryPoint
  • @Inject
  • @ViewModelInject
  • @Module
  • @Binds
  • @InstallIn
  • @Provides

2.1 @HiltAndroidApp 注释

在我们的安卓工程项目中,如果要使用Hilt,必须要有一个自定义的Application才行。这里我们的 MyApp.kt 文件中需在该类上方标注 @HiltAndroidApp

@HiltAndroidApp
class MyApp : Application() {
...
}

2.2 @AndroidEntryPoint 注释

此注释可以将成员注入到各安卓组件中,例如活动(activities)、片段(fragments)、视图(views)、服务(services)和广播接收器(broadcast receivers)。

我们必须要使用 @AndroidEntryPoint 注释来注释安卓组件,才能接下来其中继续注入字段或方法(比如 @Inject 注释)。Hilt 目前支持以下安卓类:

  • Activity(使用 @HiltAndroidApp 注释标注)
  • Fragment(使用 @AndroidEntryPoint 注释标注)
  • View(使用 @AndroidEntryPoint 注释标注)
  • Service(使用 @AndroidEntryPoint 注释标注)
  • Broadcast Receiver(使用 @AndroidEntryPoint 注释标注)

使用 @AndroidEntryPoint 注释一个安卓类需要注释所有依赖于它的类。每当我们注释一个 fragment 时,我们还必须注释使用该 fragment 的任何 activity。

比如,Activity:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

对于 Fragment:

@AndroidEntryPoint
class CountriesListFragment : Fragment() {
...
}

2.3 @Inject 注释

当项目中的构造函数被 @Inject 注释时,它就可以作为依赖项在任何地方使用。比如上面 MainActivity.kt 中的 databaseAdapter。也比如下面的例子:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    @Inject
    lateinit var repository: DbRepository

    @Inject
    lateinit var taskAdapter: TaskAdapter

    @Inject
    lateinit var task: TaskEntity

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding= ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        ...         
    }
}

我们之前提到依赖注入的三种形式:

  • constructor injection(类的构造函数注入):依赖项是通过类构造函数提供的。
  • setter/field injection(类字段注入):客户端公开一个 setter 方法,注入器使用它来注入依赖项。
  • interface injection:依赖项提供了一个注入器方法,可以将依赖项注入传递给它的任何客户端。 客户端必须实现一个接口,该接口公开一个接受依赖项的 setter 方法。

上面这个例子就是属于第二种。这里要明确说明一点,在使用 Hilt 做类字段注入的时候,变量名前面是不能加 private 关键字的。因为加了 private 之后,这个变量就变成了类私有的变量,Hilt 就没有权限访问了,也无从谈起依赖注入了。

这也就是我们在使用了 Hilt 之后,不需要像手动依赖注入时那样,在 Activity 类里面的 onCreate() 函数对这个变量进行赋值。而是使用 @inject 注释这个变量以后,直接在代码里使用。赋值的事情,是由 Hilt 帮我们代劳了。

2.4 @ViewModelInject 注释

@Inject 注释类似的,viewModel 里面的构造函数被 @ViewModelInject 注释时,它就可以作为依赖项在任何地方使用。@ViewModelInject 不是来自 Hilt 框架,而是来自 Hilt 支持的 Jetpack 库,它向 Jetpack 发出 ViewModel 已准备好注入的信号。这个注释是专门为 ViewModel 组件设计的。比如下面的例子:

@HiltViewModel
class CountriesListViewModel
@Inject constructor(
    private val repository: ApiRepository
    ) : ViewModel() {
...
}

2.5 @Module 注释

对于接口来说,接口又没有构造函数,怎么 Inject 呢?这个时候,我们就需要 @Module 注释。

Hilt module 也是一个类,但是它需要在类名前面加上注释(annotation)@Module;同时,还需要加上 @InstallIn,顾名思义,我们拆开来看这个注释:Install In,后面肯定跟的是一个作用域范围(scope),意思就是告诉Hilt,我们新建的这个 “Module” 类,作用范围是什么。这个范围(scope)的分类,我们后续将会提到,在这暂且先不用深究。

@Module
object ApiModule {
...
}

2.6 @Binds 注释

前面我们说过,接口是没有构造函数的,所以也无法使用 constructor injection 的方式进行依赖注入,那么我们怎么样才能让 Hilt 知道我们需要这个接口的实例作为依赖呢?这时候需要使用注释(annotation)@Binds来告诉 Hilt。不过有一项准备工作我们还要先做,那就是先得有一个接口的实现类。这样 Hilt 才会知道使用哪个实现类去实现这个接口,相关示例代码如下:

interface AnalyticsService {<!-- -->
  fun analyticsMethods()
}

/*接口AnalyticsService 的实现类需要 Constructor-injected,
Hilt 需要使用到*/
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService {<!-- --> ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsModule {<!-- -->

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}

AnalyticsService 是一个接口,AnalyticsServiceImplAnalyticsService 接口的实现类,同理 AnalyticsServiceImpl 的构造函数前也需要加上注释 @Inject 告诉 Hilt 在哪可以找到它。

最后就到我们的 Hilt Module 了,它前面有两个注释:@Module@InstallIn(ActivityComponent::class)。这两个注释总结起来就是告诉 Hilt,这个 Module 的可见范围是所有的Activity类。因为返回的是 AnalyticsService 接口类型,所以我们这里使用抽象类和抽象函数bindAnalyticsService 来实现这个 Hilt Module。

抽象函数 bindAnalyticsService 前面是 @Binds 注释,它的参数就是接口 AnalyticsService 的实现类 AnalyticsServiceImpl,函数返回类型就是接口 AnalyticsService。这样,Hilt就可以准确地把它的实例注入到依赖于它的地方了。

总结一下,就是对于接口实例的依赖注入,需要使用三个注释:@Module@InstallIn@Binds

2.7 @InstallIn 注释

@Module 注释的使用过程中,我们已经提到了 @InstallIn 注释的使用。通过添加 @InstallIn 注释,我们将此模块类提供的依赖对象的使用限制为特定的安卓组件。以下是另一个例子:

@Module
@InstallIn(SingletonComponent::class)
object ApiModule {
...
}

Hilt 提供了七个与 @InstallIn 注释兼容的组件。@Module 注释告诉 hilt 框架,它提供的依赖对象只能在 @InstallIn 注释中命名的特定组件的生命周期内被注入和使用。

  • @InstallIn(ApplicationComponent) — present for the lifetime of the application.
  • @InstallIn(ActivityComponent) — present for the lifetime of the Activity.
  • @InstallIn(ActivityRetainedComponent) — present for the lifetime of a configuration surviving activity (i.e) surviving orientation changes just like the ViewModel.
  • @InstallIn(FragmentComponent) — present for the lifetime of the fragment.
  • @InstallIn(ServiceComponent) — present for the lifetime of the service.
  • @InstallIn(ViewComponent) — present for the lifetime of the view that is directly inside an activity.
  • @InstallIn(ViewWithFragmentComponent) — present for the lifetime of the view inside a fragment.

2.8 @Provides 注释

当我们需要对第三方库进行依赖注入的时候,就不能使用注释(annotation)@Binds 了,为了以示区分,Hilt 提供了一个新的注释 @Provides

其实 Hilt 的注释取名还是很考究的,接口是需要“绑定”(@Binds)的,而第三方库是需要“提供”(@Provides)的。

假如一个 AnalyticsService 类实现了一个第三方库 Retrofit,这时候,我们只需要在 HiltModule 中实现一个函数,函数名可以任意起,目的是要告诉 Hilt,怎么来构建这个 AnalyticsService 类的实例对象,示例代码如下:

@Module
@InstallIn(ActivityComponent::class)
object AnalyticsModule {<!-- -->

  @Provides
  fun provideAnalyticsService(): AnalyticsService {<!-- -->
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
               .create(AnalyticsService::class.java)
  }
}

与前面的注解 @Binds 例子相比,在这个例子中主要是把 @Binds 改成了 @Provides,函数 provideAnalyticsService 返回的是一个使用 Retrofit 构建的 AnalyticsService 类的实例。

这样,Hilt 就可以在需要 AnalyticsService 类实例(依赖)的地方,把它的实例注入进去。

3 Hilt 的简单案例

在这里,我们介绍一个非常简单的 Hilt 案例,大家可以跟着我们一起在 Android Studio 上新建一个安卓的应用,然后按照下面的步骤一步步添加相关依赖和代码。

3.1 AndroidManifest.xml

Hilt 框架会在我们首次构建项目时为项目创建一个基类 Hilt_MyApp。 我们不需要直接扩展基类,因为它会自动创建带注释的类 MyApp。这里我们需要在 AndroidManifest.xml 文件中添加 android:name=".MyApp",如下所示:

<application
        android:name=".MyApp"
        ... >
     <activity android:name=".MainActivity">
       <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
     </activity>
</application>

3.2 build.gradle (Project)

我们需要在 build.gradle (Project) 中添加 Hilt 对应的 classpath。代码如下:

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
    dependencies {
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.43.2'
    }
}

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.2.0' apply false
    id 'com.android.library' version '7.2.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

3.3 build.gradle (Module)

我们需要在 build.gradle (Module) 中添加 Hilt 对应的依赖

implementation 'com.google.dagger:hilt-android:2.43.2'
kapt 'com.google.dagger:hilt-compiler:2.43.2'

并且 plugins 中也需要添加:

id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'

代码如下:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.hiltex1"
        minSdk 27
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.7.0'
    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

    //Hilt
    implementation 'com.google.dagger:hilt-android:2.43.2'
    kapt 'com.google.dagger:hilt-compiler:2.43.2'
}

3.4 Application Class MyApp.kt

在我们的安卓工程项目中,如果要使用Hilt,必须要有一个自定义的Application才行。这里我们的 MyApp.kt 文件中需在该类上方标注 @HiltAndroidApp

@HiltAndroidApp
class MyApp : Application() {
...
}

3.5 MainActivity.kt

在这里,我们写一个很简单的 MainActivity.kt 文件,代码如下:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseAdapter: DatabaseAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        Log.d(TAG,"DatabaseAdapter : $databaseAdapter")
        databaseAdapter.log("Hey Hilt")
    }
}

这里我们使用到了 @AndroidEntryPoint 注释。AndroidEntryPoint 也就是上是其所相关联的依赖项的入口点(EntryPoint)。 我们需要告诉 Hilt 我们希望在何处注入依赖项。这就是AndroidEntryPoint的作用。它定义了将在何处提供依赖项。

Hilt 在这里的主要目的是尝试标准化注入功能,并消除尽可能多的组件耦合。AndroidEntryPoint 会自动为系统构建这些依赖项和组件。 所以我们不必再像前一章节那样去进行手动的依赖注入。当然,需要再强调一次,如果我们使用 AndroidEntryPoint 注释 fragment 并且它包含依赖项,则还必须注释使用到该 fragment 的 Activities。即,任何依赖于我们正在注释的这个类的相关类,依赖项,组件等,同样需要被注释。

在我们往下看其他代码之前,关注一下这行代码:

@Inject
lateinit var databaseAdapter: DatabaseAdapter

看似平平无奇,但实际上,这就是前面提到依赖注入的三种形式之一:setter/field injection(类字段注入)。在这个例子中,我们希望将 databaseAdapter 注入到 main activity 中。

3.6 DatabaseAdapter.kt

这里有提到依赖注入的三种形式之一:constructor injection(类的构造函数注入)。在 DatabaseAdapter.kt 中,我们就要使用到:

class DatabaseAdapter @Inject constructor(var databaseService: DatabaseService) {
    fun log(msg: String){
        Log.d(TAG,"DatabaseAdapter : $msg")
        databaseService.log(msg)
    }
}

创建依赖(dependency)的最基本、最简单的方法是通过构造函数注入。 一个组件(component)本质上是一个不带参数的类,我们只需要添加 inject 注解,这样就可以随时将其注入到需要的地方(又比如 class SampleClass @Inject constructor())。

3.7 DatabaseService.kt

这也是一个类的构造函数注入的例子。

class DatabaseService @Inject constructor() {
    fun log(msg: String){
        Log.d(TAG,"DatabaseService msg : $msg")
    }
}

大家有没有发现,在用了 Hilt 之后,依赖注入的使用变得非常简单,代码也变得非常简洁(我们可以对比一下系列2中的手动依赖注入案例)。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

破浪会有时

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值