如何在Android中使用Hilt依赖注入框架

87 阅读10分钟

在Android中使用Hilt依赖注入框架

依赖性是指另一个对象需要的对象。换句话说,后一个对象依赖于前一个对象才能发挥作用。

依赖关系简介

依赖注入是指将依赖关系提供给一个类,而不是由该类自己来创建它们。Hilt是一种在Android应用程序中执行依赖注入的标准化方式。

目标

本教程的目的是

  1. 定义依赖性注入。
  2. 解释为什么依赖性注入很重要。
  3. 详细展示如何使用Hilt进行依赖注入。

前提条件

  1. 对面向对象编程有基本了解
  2. 对使用Kotlin的Android应用开发有基本了解。
  3. Android Studio 4.0或更高版本。

目录

  1. 手动依赖注入。
  2. 使用Hilt的依赖注入。
  3. Hilt和接口。
  4. Hilt与第三方库
  5. 结语

第一部分:手动依赖注入

开始使用

注意,每一步都有其分支。

在Android Studio中创建一个新项目,并将其命名为Hilt Tutorial

接下来,创建以下EnglishPerson 类。

class EnglishPerson {
 fun speakEnglish(){
 Log.i("EnglishPerson","Hello kind sir.")
    }
}

创建第二个类,并将其命名为SpanishPerson

class SpanishPerson {
 fun speakSpanish(){
 Log.i("SpanishPerson","Despacito senor")
    }
}

由于英语是使用最广泛的语言,我们需要西班牙人学习英语。

一个可能的解决方案是将EnglishPerson 类实例化为SpanishPerson 类。

然而,这并不可取。

class SpanishPerson {
 val englishPerson = EnglishPerson()
 fun speakSpanish(){
 Log.i("SpanishPerson","Despacito senor")
    }
}

现在,这个西班牙人也是用英语。这就是字段注入

然而,这被证明是一种构建类的糟糕方式。它违反了Single Responsibility Principle :一个西班牙语类不应该关注英语事务!这就是依赖注入的作用。

这就是依赖性注入的作用。

SpanishPerson 类进行如下修改。

class SpanishPerson(val englishPerson: EnglishPerson) {
 fun speakSpanish(){
 Log.i("SpanishPerson","Despacito senor")
    }
}

为了使SpanishPerson ,它需要一个依赖关系;EnglishPerson 。这就是依赖性注入构造器注入

事实证明,以这种方式设置你的代码有几个好处。当西班牙人学习一种新的语言时,你只需将其添加到构造函数中。

你不需要不断地改变SpanishPerson里面的代码。因此,代码的可维护性和灵活性更高。这也使得它更具有可测试性和可扩展性。

我们可以在MainActivity中运行以下代码。

class MainActivity : AppCompatActivity() {
 private lateinit var spanishPerson: SpanishPerson
 private lateinit var englishPerson: EnglishPerson
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        englishPerson = EnglishPerson()
        spanishPerson = SpanishPerson(englishPerson)
        spanishPerson.speakSpanish()
        spanishPerson.englishPerson.speakEnglish()
    }
}

运行你的应用程序并打开logcat 。然后,搜索EnglishPersonSpanishPerson

2021-05-04 20:18:57.663 20540-20540/com.example.android.hilttutorial I/EnglishPerson: Hello kind sir.

2021-05-04 20:35:28.164 21573-21573/com.example.android.hilttutorial I/SpanishPerson: Despacito senor

MainActivity ,有几件事需要注意。

  1. MainActivity 初始化其依赖关系。因此,它的依赖关系只在其生命周期内可用。这使得 成为一个MainActivity 组件

  2. MainActivity 同时也托管了依赖项。这使得它成为一个依赖性容器

第二部分:Hilt的依赖注入

手动依赖注入是可行的。然而,随着应用程序的扩展,管理依赖关系变得很麻烦。虽然Hilt的设置成本很高,但在扩展应用程序时,它是相当有益的。

开始使用Hilt

在项目的根 build.gradle 文件中,添加以下语句。

buildscript {
    ...
    ext.hilt_version = '2.35' //check for most recent version
    dependencies {
        ...
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}

在app/build.gradle文件中,包括。

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

android {
    ...
}

dependencies {
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"
}

创建一个扩展Application() 的新类,并对其做如下注释。

@HiltAndroidApp
class MyApplication:Application() {
}

这使得Hilt ,可以访问整个应用程序。它在应用程序层面上创建了一个依赖性容器。换句话说,Hilt可以为应用程序的任何部分提供依赖性。

AndroidManifest.xml 文件中的应用程序标签下添加以下代码。

 <application
 android:name=".MyApplication"
        ...>

这通知清单参考与Hilt连接的应用类。

创建 Hilt 依赖关系

EnglishPerson 类做此修改。

class EnglishPerson @Inject constructor(){
    ...
}

@Inject 让Hilt访问 '的构造函数。这意味着,现在 Hilt 可以生成 的实例。EnglishPerson EnglishPerson

MainActivity 做如下修改。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
 @Inject
    lateinit var englishPerson: EnglishPerson
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        englishPerson.speakEnglish()
    }
}

@Inject 这里有一个不同的目的。它标识了 字段。Injectable 意味着 Hilt 可以向它提供实例化的依赖关系。injectable

注意,你现在不需要实例化EnglishPerson() 类。

@AndroidEntryPoint 已经登场了。它标识了依赖性容器。你将在这里获得你的依赖关系。

注意@AndroidEntryPoint 注释了Activities,Fragments,Views,ServicesBroadcastReceivers 。它把它们变成了依赖性容器。

运行你的应用程序并打开logcat。

搜索EnglishPerson

com.example.android.hilttutorial I/EnglishPerson: Hello kind sir.

SpanishPerson 做如下修改。

class SpanishPerson @Inject constructor(val englishPerson: EnglishPerson) {
    ...
}

@Inject 这里的作用与 相同。它让 Hilt 访问 的构造函数。然后Hilt可以生成一个 的实例。EnglishPerson SpanishPerson SpanishPerson

然而,这并不像第一种情况那样简单。为了创建SpanishPerson ,它还需要创建EnglishPerson 。这是因为SpanishPerson 在其构造函数中需要EnglishPerson 作为参数。

Hilt已经知道如何创建EnglishPerson 。所以一切都很好。

Hilt知道如何创建的实例以绑定的名义进行。

因此,EnglishPersonSpanishPerson 是绑定的。

MainActivity 做如下修改。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
 @Inject
    lateinit var spanishPerson: SpanishPerson
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        spanishPerson.speakSpanish()
        spanishPerson.englishPerson.speakEnglish()
    }
}

简洁而美丽!

运行应用程序并打开logcat。

搜索SpanishPersonEnglishPerson

com.example.android.hilttutorial I/SpanishPerson: Despacito senor
com.example.android.hilttutorial I/EnglishPerson: Hello kind sir.

第三部分:刀柄和接口

西班牙人、英国人,为什么如此分裂?我们都是人!

创建以下界面。

interface Person {
 fun speakLanguage()
}

修改EnglishPerson

class EnglishPerson @Inject constructor(): Person {
 override fun speakLanguage() {
 Log.i("EnglishPerson", "Hello kind sir")
    }
}

修改MainActivity

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
 @Inject
    lateinit var englishPerson: Person //Note that EnglishPerson is replaced with Person
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        englishPerson.speakLanguage()
    }
}

在你运行代码之前,你可能会问。

为什么要用更通用的Person 类型来替换更具体的EnglishPerson

使用接口的类型可以非常有用。一个用例是,以这种方式创建的代码是非常可测试的。使用Person 这个类型使得在测试过程中很容易用一个假的类型来替换它。

然而,当你运行你的应用程序时,它在编译时崩溃了。Hilt无法实现该接口。它不知道如何实现。接口不像类那样有构造函数。不可能@Inject它们。

你需要让Hilt掌握如何实现接口的知识。创建一个带有以下注解的abstract 类。

@Module
@InstallIn(ActivityComponent::class)
abstract class PersonModule{

}

一个模块通知Hilt当它不能访问构造函数时如何提供依赖。@Module ,用于识别模块。

@InstallIn(ActivityComponent) 声明只有在活动处于活动状态时,下面的实现才是活的。因此,该活动是组件。

在模块内部创建一个abstract 函数。

@InstallIn(ActivityComponent::class)
@Module
abstract class PersonModule {
 @Binds
 abstract fun EnglishPersonImpl(englishPerson: EnglishPerson):Person
}

@Binds 告诉Hilt,当它需要提供一个接口的实例时,应该使用哪个实现。关于如何提供实现的信息在函数参数中。

由于 Hilt 已经知道如何实现EnglishPerson ,所以一切都很顺利。

运行代码并打开logcat。

搜索 "EnglishPerson"。

com.example.android.hilttutorial I/EnglishPerson: Hello kind sir

现在修改SpanishPerson

class SpanishPerson @Inject constructor():Person {
 override fun speakLanguage() {
 Log.i("SpanishPerson","Despacito senor")
    }
}

MainActivity 中做如下修改。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
 @Inject
    lateinit var spanishPerson: Person
 override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        spanishPerson.speakLanguage()
    }
}

运行你的应用程序并打开logcat。搜索'SpanishPerson'。

没有结果?

试试搜索'EnglishPerson'

com.example.android.hilttutorial I/EnglishPerson: Hello kind sir

似乎Hilt正在使用EnglishPersonImpl ,将SpanishPeople 的实例生成为EnglishPeople!

我们需要以某种方式来区分它们。

PersonModule 类之外但在同一文件中添加以下代码。

@Qualifier
annotation class EnglishQualifier

@Qualifier
annotation class SpanishQualifier

你将使用这些限定词来区分EnglishSpanish 的实现。

创建另一个abstract 函数,实现SpanishPerson

@SpanishQualifier
@Binds
abstract fun SpanishPersonImpl(spanishPerson:SpanishPerson):Person

同时,将@EnglishQualifier 添加到EnglishPersonImpl

PersonModule 的最终代码看起来像这样。

@InstallIn(ActivityComponent::class)
@Module
abstract class PersonModule {

 @EnglishQualifier
 @Binds
 abstract fun EnglishPersonImpl(englishPerson: EnglishPerson):Person

 @SpanishQualifier
 @Binds
 abstract fun SpanishPersonImpl(spanishPerson:SpanishPerson):Person
}

@Qualifier
annotation class EnglishQualifier

@Qualifier
annotation class SpanishQualifier

前往MainActivity ,并做一个小改动。

添加限定词。

@SpanishQualifier
@Inject
lateinit var spanishPerson:Person

运行你的应用程序并打开logcat。

搜索SpanishPerson

com.example.android.hilttutorial I/SpanishPerson: Despacito senor

该代码现在可以工作了。

第四部分:Hilt和第三方库

当我们可以访问构造函数时,Hilt工作得很好。但如果你不能访问构造函数呢?这发生在你导入第三方库的时候。你并不拥有这些类。派对停止了吗?

请导入以下Gson 库。

  implementation 'com.google.code.gson:gson:2.8.6'

创建一个GsonModule,如下所示。

@Module
@InstallIn(ActivityComponent::class)
object GsonModule {
 @Provides
 fun provideGson(): Gson {
 return Gson()
    }
}

通过@Provides ,注释的函数给Hilt提供了以下信息。

  • 返回类型告诉Hilt这个函数提供什么类型的实例。

  • 参数告诉Hilt提供该类型所需的依赖性。在我们的例子中,没有任何依赖。

  • 函数主体告诉Hilt如何提供一个相应类型的实例。Hilt每次需要提供该类型的实例时都会执行函数体。

MainActivity 做如下修改。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
        lateinit var gson: Gson
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    Log.i("MainActivityGson",gson.toString())
        }
}

运行你的应用程序并打开logcat。搜索MainActivityGson

你会发现有很多字,这并不重要。重点是,你已经成功注入了第三方库的依赖关系。

当涉及到诸如Gson,RetrofitRoom database 等类时,我们可能需要让它们在整个应用程序中可用。

尝试将Gson 依赖关系注入到MyApplication

@HiltAndroidApp
class MyApplication:Application() {
 @Inject
    lateinit var gson:Gson
 override fun onCreate() {
 super.onCreate()
 Log.i("MyApplicationGson",gson.toString())
    }
}

运行你的应用程序。

编译时出错。

你还记得关于组件的讨论吗?如果你看一下GsonModule 组件,它被安装在ActivityComponent.class 。因此,它只在一个活动的生命周期内可用,而不是整个应用程序的生命周期。

为了纠正这个错误,把ActivityComponent.class 改为SingletonComponent.class

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

运行你的应用程序并打开logcat。当你,搜索MyApplicationGson ,你会得到同样长而奇怪的字符串。

但是Gson 对象在MyApplicationMainActivity 中是一样的吗?

一个简短的答案。不是。

Hilt中的绑定是naturally unscoped 。这意味着每当需要一个依赖关系时,Hilt就会实例化一个新的依赖关系。

为了确保每次只有一个Gson 的实例可用,请对GsonModule 做如下修改。

@Module
@InstallIn(SingletonComponent::class)
object GsonModule {
 @Singleton
 @Provides
 fun provideGson(): Gson {
 return Gson()
    }
}

@Singleton 是用于确保生成的实例在整个应用程序的生命周期中是唯一的实例的注解。

ActivityScoped 确保该实例在整个活动中是相同的。

关于作用域的更多信息,请查看Android文档

总结

本教程从说明手动依赖注入开始。手动依赖性注入是好的。但是,随着应用程序的扩展,它可能会变得很麻烦。

然后,Hilt带着它的@Inject 注解进来了,它创建了injectable 字段、methodsconstructors@Inject 也帮助Hilt知道如何通过让它访问构造函数来提供某个类。

你观察到构造函数可能不可用的情况。

这些情况包括。

  1. 当使用一个接口时。
  2. 当使用第三方库的时候。

当构造函数不可用时,必须使用一个模块。模块是一个告诉Hilt如何提供一个实例的类。它需要被安装在一个组件中。这使得它可以跟踪模块的生命周期。

@Binds 提供了接口的实现。该实现是一个Hilt知道如何提供的类。

@Provides 提供第三方库的实现。Hilt每次都会运行函数体以获得所需的实例。

希望这篇文章能对Hilt的依赖注入有所启发。