本文是Assisted Injection With Dagger and Hilt博客的译文,英语好的同学可以直接阅读原文,不需要翻墙,文章中的源码也在原博客中。
以下是原文的翻译。
使用Dagger和Hilt辅助注入
本文通过学习辅助注入的用处、原理,以及如何通过Dagger的新的构建方式来给你的app添加辅助注入。
前言
使用Dagger进行依赖注入是Android社区的一个热门话题。Dagger和它的新的拓展-Hilt都是不断改进的开源项目,每天都有新的功能和提升加入。辅助注入(Assisted Injection)就是Dagger从2.31版本开始加入的新功能。
在这个教程中,你将会学习:
- 辅助注入是什么,以及为什么它会有用
- 在Dagger2.31版本之前如何通过AutoFactory使用辅助注入
- Dagger2.31+中辅助注入的工作原理
- 如何通过Hilt和ViewModels使用辅助注入
提示:本教程假设了你熟悉Android开发和Android Studio的使用。如果不是,请先阅读 Android开发入门
和 Android版Kotlin 教程。
本教程是Dagger系列文章中的一文,如果你不熟悉Dagger,请先阅读这些资源:
- Android中的Dagger2教程:进阶
- Android中的Dagger2教程:进阶2
- 从Dagger迁移到Hilt
- 全新的Dagger教程
现在让我们开始学习!
开始
首先通过点击本教程上方或者下方的Download Matrerails按钮下载最新的项目版本(译者注:请到原文链接中下载),然后在Android Studio打开,你将会看到下面的资源目录:
这是 AssistedGallery 项目的架构,你将通过这个项目学习辅助注入,构建(Build)并运行(Run)这个app你将会看到它是如何工作的。在App中你将会看到如下的内容:
提示: 在你的设备上,图片可能有所不同。这是因为应用使用placeimg.com服务,它提供了一个简单的接口,能够根据提供的尺寸和话题获取随机的图片。
现在项目搭建好了,让我们来看一下app的架构
AssistedGallery App架构
AssistedGallery是一个简单的应用,内部实现和使用了ImageLoader。
在看代码之前,先看一眼下面的图,其中描述了不同组件之间的依赖关系。当你要讨论依赖注入时,理解这个app主要组件之间的依赖关系是很必要的。
在这张图中,你可以看到:
ImageLoader
类:通过提供的URL下载图片加载到ImageView
ImageLoader
基于BitmapFetcher
实现,BitmapFetcher
用来处理从网络上获取的Bitmap
数据。这部分是怎么实现的在本教程中并不重要。- 访问网络和其他密集型的IO操作必须放到后台线程,所以
ImageLoader
依赖两个CoroutineDiapathcer
实例。 - 最后,表中还有一个通过不同的
ImageFilter
接口实现,来执行Bitmap
变换的操作。这部分过滤器的实现也不重要。
继续阅读以了解如何在代码中表示这一点。
ImageLoader 类
要了解ImageLoader
是如何工作的,先打开 bitmap包下的 ImageLoader.kt文件并查看代码。其中包含两个主要部分:
- 使用构造函数注入管理依赖项。
- 实现
loadImage
功能。
前面的图有助于了解如何实现构造函数注入。
使用构造函数注入管理依赖项
构造函数注入是在类中注入依赖项的好方法,这种方法会在你创建实例时进行注入,并使参数不可变。举个例子,如ImageLoader的主构造函数:
class ImageLoader constructor(
private val bitmapFetcher: BitmapFetcher, // 1
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher, // 2
@Dispatchers.Main private val uiDispatcher: CoroutineDispatcher, // 2
@DrawableRes private val loadingDrawableId: Int = R.drawable.loading_animation_drawable, // 3
private val imageFilter: ImageFilter = NoOpImageFilter // 4
) {
// ...
}
上面的代码有很多值得注意的地方:
ImageLoader
依赖一个BitmapFetcher
接口的实现类,作为它构造函数第一个接收的参数。像所有参数一样,它是private val,私有只读(译者注:非强制).- 你需要两种不同的
CoroutineDispatcher
实现。第一个用@Dispatchers.IO
注释,你可以使用它进行后台操作,例如访问网络或转换Bitmap
. 第二个用@Dispatchers.Main
注释,你可以使用它与 UI 进行交互。 - 前面的参数都是必传参数,
loadingDrawableId
是第一个可选参数,代表后台作业正在进行时要显示的Drawable
。 - 最后,还有一个可选的
ImageFilter
参数,用于转换从网络加载的Bitmap
。
注意:这里的可选参数意思是非强制传递参数,因为它有一个默认值。
实现 loadImage 方法
虽然这部分不是必要的,不过为了完整性,查看下面loadImage
实现的代码也是有用的:
class ImageLoader constructor(
// ...
) {
suspend fun loadImage(imageUrl: String, target: ImageView) =
withContext(bgDispatcher) { // 1
val prevScaleType: ImageView.ScaleType = target.scaleType
withContext(uiDispatcher) { // 2
with(target) {
scaleType = ImageView.ScaleType.CENTER
setImageDrawable(ContextCompat.getDrawable(target.context, loadingDrawableId))
}
}
val bitmap = bitmapFetcher.fetchImage(imageUrl) // 3
val transformedBitmap = imageFilter.transform(bitmap) // 4
withContext(uiDispatcher) { // 5
with(target) {
scaleType = prevScaleType
setImageBitmap(transformedBitmap)
}
}
}
}
在这部分代码中,你可以:
-
使用
withContext
在后台线程的上下文中运行包含的代码。 -
切换到 UI 线程以在加载和转换
Bitmap
时设置对应的Drawble
. -
在后台线程的上下文中,从网络获取
Bitmap
的数据。 -
变换
Bitmap
. 由于这是一项开销很大的操作,因此需要在后台线程的上下文中执行它。 -
返回到 UI 线程以显示
Bitmap
.
现在,如何提供ImageLoader
所需要的依赖项并使用呢?
使用ImageLoader类
打开ui目录下的MainActivity.kt文件,查看以下代码:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
@Dispatchers.IO
lateinit var bgDispatcher: CoroutineDispatcher // 1
@Inject
@Dispatchers.Main
lateinit var mainDispatcher: CoroutineDispatcher // 2
@Inject
lateinit var bitmapFetcher: BitmapFetcher // 3
@Inject
lateinit var imageUrlStrategy: ImageUrlStrategy // 4
lateinit var mainImage: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainImage = findViewById<ImageView>(R.id.main_image).apply {
setOnLongClickListener {
loadImage()
true
}
}
}
override fun onStart() {
super.onStart()
loadImage()
}
fun loadImage() { // 5
lifecycleScope.launch {
ImageLoader(
bitmapFetcher,
bgDispatcher,
mainDispatcher
)
.loadImage(imageUrlStrategy(), mainImage)
}
}
}
在代码中,可以看到
- 使用了
@Dispatchers.IO
作为限定符,用于注入后台线程的CoroutineDispatcher
。 - 使用
@Dispatchers.Main
作为限定符,修饰主线程的CoroutineDispatcher
。 - 注入一个
BitmapFetcher
. - 注入一个
ImageUrlStrategy
对象,它创建要下载图像的 URL。 - 使用所有依赖项创建一个
ImageLoader
实例,并将图像加载到ImageView
。
这些代码显然太多了,尤其是在使用依赖注入的时候。你真的需要将所有依赖项都注入MainActivity吗?
只注射你需要的
为了简化代码,你不需要将ImageLoader
所需要的全部依赖项都注入到MainActivity
.作为替代,你可以只注入ImageLoader
本身,让 Dagger 来完成困难的部分。
在di包中新建一个名为ImageLoaderModule.kt的文件,编写如下代码:
@Module
@InstallIn(ActivityComponent::class)
object ImageLoaderModule {
@Provides
fun provideImageLoader(
@Dispatchers.IO bgDispatcher: CoroutineDispatcher,
@Dispatchers.Main mainDispatcher: CoroutineDispatcher,
bitmapFetcher: BitmapFetcher
): ImageLoader = ImageLoader(
bitmapFetcher,
bgDispatcher,
mainDispatcher
)
}
在代码中,将ImageLoader
的实例添加到activity scope(译者注:Hilt的作用域之一)的依赖中。
然后就能将MainActivity.kt 中的代码更新为以下内容:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var imageLoader: ImageLoader // 1
@Inject
lateinit var imageUrlStrategy: ImageUrlStrategy
lateinit var mainImage: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainImage = findViewById<ImageView>(R.id.main_image).apply {
setOnLongClickListener {
loadImage()
true
}
}
}
override fun onStart() {
super.onStart()
loadImage()
}
fun loadImage() {
lifecycleScope.launch {
imageLoader.loadImage(imageUrlStrategy(), mainImage) // 2
}
}
}
如你所见,在新的代码中:
- 将
ImageLoader
直接注入到imageLoader
实例变量中。 - 使用
imageLoader
来加载要显示的图像。
构建并运行,然后检查一切是否仍按预期工作。
注意:和之前一样,你会看到API提供的随机图像。
其他参数呢?
到目前为止一切都很好但是……是的,还有一个但是。:] ImageLoader
还有两个可选参数。如果想要ImageLoader
像刚才那样注入,如何传递loadingDrawableId
,imageFilter
的值呢?
一种解决方案是让loadImage
中的参数loadingDrawableId
和imageFilter
如下所示:
suspend fun loadImage(
imageUrl: String,
into: ImageView,
@DrawableRes loadingDrawableId: Int = R.drawable.loading_animation_drawable,
imageFilter: ImageFilter = NoOpImageFilter) { /*... */ }
这是一个完全可行的解决方案,但它对依赖注入没有意义。这是因为(译者注:这种场景下这两个参数不应该是固定的)你需要在每次加载新图片时传递Drawable
和ImageFilter
参数。更好的方法是在ImageLoader
刚创建时传递。
你想创建一个ImageLoader
实例,并使用 Dagger 为你管理的一些参数,同时在创建实例时,自己再传入的一些参数。这就是 辅助注入,Dagger从 2.31 版本开始原生支持。但是,许多代码库不是最新版本,因此需要首先了解如何在早期版本的 Dagger 中使用辅助注入。
使用 AutoFactory 进行辅助注射
在 Dagger 2.31 之前, 你可以通过AutoFactory实现辅助注入,这是一个为Java创建的代码生成器。
但它也适用于 Kotlin,只是有一些限制。
在查看代码之前,有必要了解一下 AutoFactory和其他辅助注入工具的作用。假设你有一个与之前的 ImageLoader完全一样的,具有一些依赖项的类:
class ImageLoader constructor(
private val bitmapFetcher: BitmapFetcher,
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher,
@Dispatchers.Main private val uiDispatcher: CoroutineDispatcher,
@DrawableRes private val loadingDrawableId: Int = R.drawable.loading_animation_drawable,
private val imageFilter: ImageFilter = NoOpImageFilter
) {
// ...
}
这个类有五个主要的构造函数参数。如前所示,Dagger 可以为前三个提供实例。这意味着,如果你要创建ImageLoader
的实例,你只需要传入最后两个参数。你会怎么样做呢?这就是 Factory Method发挥作用的地方。
你可以注入一个 Factory,而不是注入整个 ImageLoader,如下所示:
现在,将其转换为代码:
class ImageLoaderFactory @Inject constructor( // 1
private val bitmapFetcher: BitmapFetcher, // 2
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher, // 2
@Dispatchers.Main private val uiDispatcher: CoroutineDispatcher // 2
) {
fun create(
@DrawableRes private val loadingDrawableId: Int = R.drawable.loading_animation_drawable, // 3
private val imageFilter: ImageFilter = NoOpImageFilter // 3
) = ImageLoader(bitmapFetcher, bgDispatcher, uiDispatcher, loadingDrawableId, imageFilter) // 4
}
在这部分代码中,可以看到:
ImageLoaderFactory
的主构造函数有一个@Inject
注解:Dagger需要知道如何创建一个它的实例。- Dagger需要提供的参数,是
ImageLoaderFactory
的主构造函数的参数。 - 你需要提供的依赖项是
create()
的参数. create()
组合所有参数以创建ImageLoader
现在,你已经指定哪些参数由Dagger提供,哪些参数在调用create()
时由你传递。 AutoFactory将根据这些信息为你生成 Factory的代码。
配置AutoFactory
AutoFactory 使用注解处理来生成代码。
在 app 中打开 build.gradle并将以下几行添加到dependencies
中
implementation 'com.google.auto.factory:auto-factory:1.0-beta5@jar' // 1
kapt 'com.google.auto.factory:auto-factory:1.0-beta5' // 2
compileOnly 'javax.annotation:jsr250-api:1.0' // 3
在这部分代码中,你:
- 添加了代码中用到的注解所在的依赖项。
- 使用 kapt,设置注释处理器,该处理器将生成用于辅助注入的代码。
- 添加一些 AutoFactory 生成的代码将使用的注释(例如,
@Generated
)。这里使用compileOnly
,因为这些仅在编译期间才需要。
在同一个 build.gradle文件中,在dependencies
块上方添加以下定义:
这可以在stubs中进行错误类型推断。这很有用,因为AutoFactory注解处理器依赖于声明签名中的精确类型。如果没有这个定义,Kapt 会用NonExistentClass
替换每个未知类型,当代码生成过程中出现这种问题时,调试将变得非常困难。
在 AssistedGallery 应用程序中使用 AutoFactory
将依赖项添加到 app 中的 build.gradle文件后,
项目中将可以使用以下注释:
- @AutoFactory:标记你要使用辅助注入提供的类型。
- @Provided:标记创建实例时 Dagger 提供的参数。
注意:当然,不要忘记从Android Studio的File菜单中选择Sync Project with Gradle files同步项目
为辅助注射准备课程
在ImageLoader
使用 AutoFactory很简单。打开 bitmap包中的 ImageLoader.kt
并像下面一样更改类和构造函数,保持内部实现的代码不变
@AutoFactory // 1
class ImageLoader constructor(
@Provided
private val bitmapFetcher: BitmapFetcher, // 2
@Provided
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher, // 2
@Provided
@Dispatchers.Main private val uiDispatcher: CoroutineDispatcher, // 2
@DrawableRes private val loadingDrawableId: Int = R.drawable.loading_animation_drawable, // 3
private val imageFilter: ImageFilter = NoOpImageFilter // 3
) {
// ...
}
在这部代码中。你可以:
- 用
@AutoFactory
注释类名,以便 AutoFactory 处理它并生成代码。 - 使用
@Provided
注释bitmapFetcher
,bgDispatcher
和uiDispatcher
这三个构造函数的参数。这样他们就被标记为 Dagger 需要提供的参数。 - 不要注释
loadingDrawableId
和imageFilter
。这些是你在使用工厂创建ImageLoader
实例时需要提供的构造函数参数。
查看生成的代码
要了解如何使用ImageLoader
,你需要构建应用程序并查看 build/generated/source/kapt/debug目录中的生成代码,如下图所示:
注意:切换到Project视图以查看build文件夹。
如果你打开ImageLoaderFactory.java,你将看到以下内容:
如果打开 ImageLoaderFactory.java,你会看到下面的内容:
@Generated( // 1
value = "com.google.auto.factory.processor.AutoFactoryProcessor",
comments = "https://github.com/google/auto/tree/master/factory"
)
public final class ImageLoaderFactory {
private final Provider<BitmapFetcher> bitmapFetcherProvider; // 2
private final Provider<CoroutineDispatcher> bgDispatcherProvider; // 2
private final Provider<CoroutineDispatcher> uiDispatcherProvider; // 2
@Inject // 4
public ImageLoaderFactory(
Provider<BitmapFetcher> bitmapFetcherProvider, // 3
@Schedulers.IO Provider<CoroutineDispatcher> bgDispatcherProvider, // 3
@Schedulers.Main Provider<CoroutineDispatcher> uiDispatcherProvider) { // 3
this.bitmapFetcherProvider = checkNotNull(bitmapFetcherProvider, 1);
this.bgDispatcherProvider = checkNotNull(bgDispatcherProvider, 2);
this.uiDispatcherProvider = checkNotNull(uiDispatcherProvider, 3);
}
public ImageLoader create(int loadingDrawableId, ImageFilter imageFilter) { // 5
return new ImageLoader(
checkNotNull(bitmapFetcherProvider.get(), 1),
checkNotNull(bgDispatcherProvider.get(), 2),
checkNotNull(uiDispatcherProvider.get(), 3),
loadingDrawableId,
checkNotNull(imageFilter, 5));
}
// ...
}
AutoFactory生成的这段 Java 代码包含很多有趣的东西:
-
@Generated
注释提供关于所生成的文件的元数据。 -
每个使用
@Provided
注释的构造函数参数的都用final字段修饰,使用工厂的构造函数时初始化这些参数。 -
构造函数上
@Inject
注解意味着Dagger将创建ImageLoaderFactory
实例。 -
AutoFactory生成了
create()
方法,方法的参数是未用@Provided
标记的参数。方法内的实现非常简单,其使用构造函数的值和作为create()其自身参数的值,创建了一个ImageLoader
实例。
现在是时候在MainActivity
中使用ImageLoaderFactory
了。
使用生成的工厂
在 ui包中打开 MainActivity.kt并做以下更改:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var imageLoaderFactory: ImageLoaderFactory // 1
// ...
fun loadImage() {
lifecycleScope.launch {
imageLoaderFactory
.create( // 2
R.drawable.loading_animation_drawable,
GrayScaleImageFilter()
).loadImage(imageUrlStrategy(), mainImage)
}
}
}
在这部分代码中,你:
- 注入
ImageLoaderFactory
代替ImageLoader
。 - 调用
create()
,传入Drawable
和ImageFilter
以创建ImageLoader
的实例。在这个例子中,你使用 GrayScaleImageFilter作为 ImageFilter实现。
注意:由于你现在注入了
ImageLoaderFactory
,就可以删除 di包中的 ImageLoaderModule.kt。
构建并运行应用程序以查看新的灰度过滤器的运行情况。
这意味着 Dagger 通过 Factory提供了一些依赖项,你提供剩余的依赖项作为create()
的参数.
注意:你可能会想:
Drawable
加载时显示的参数ImageFilter
曾经有默认值。那些去了哪里?Java 没有参数默认值的概念,因此注解处理器不知道它们存在。你可能认为使用@JvmOverloads
会为 生成不同的create()
重载方法,但不幸的是,目前尚不支持。
Dagger2.31+的辅助注射
如果你使用Dagger2.31或更高的版本,则可以从辅助注入中受益,而无需任何其他依赖项。
正如你接下来会看到的,你可以通过使用不同的注释来获得与 @AutoFactory相同的结果。
要迁移使用Dagger的 辅助注射,你需要:
- 删除 AutoFactory 的依赖并更新 Dagger/Hilt 的版本。
- 分别使用
@AssistedInject
和@Assisted
代替@AutoFactory
和@Provided
。 - 定义一个 Factory实现,并使用
@AssistedFactory
对其注释。
现在可以将ImageLoader
上AutoFactory的辅助注入迁移 Dagger 上的辅助注入了。
更新依赖项
作为第一步,打开应用模块的 build.gradle,并删除之前添加的定义:
// START REMOVE
implementation 'com.google.auto.factory:auto-factory:1.0-beta5@jar'
kapt 'com.google.auto.factory:auto-factory:1.0-beta5'
compileOnly 'javax.annotation:jsr250-api:1.0'
// END REMOVE
之后,升级 Hilt 的版本。在撰写本文时,这是 2.33-beta。你还可以检查 MavenCentral以获取最新的可用版本。
要更新 Hilt 的版本,请更改 hilt_android_version的值。打开项目级 build.gradle文件并更新版本:
buildscript {
ext.kotlin_version = "1.4.31"
ext.hilt_android_version = "2.33-beta" // Update this value
repositories {
google()
mavenCentral()
}
// ...
}
// ...
注意:在对 build.gradle文件进行更改后,不要忘记将你的项目与 Gradle 同步。
在开始之前,打开 ApplicationModule.kt,然后将替换两个 ApplicationComponent::class的引用
都替换成 SingletonComponent::class。因为这一块在新版本的Dagger中被重新命名了。
@Module(includes = arrayOf(Bindings::class))
@InstallIn(SingletonComponent::class) // Check this line
object ApplicationModule {
// ...
@Module
@InstallIn(SingletonComponent::class) // Check this line
interface Bindings {
// ...
}
}
你的代码应该如上所示。
使用@AssistedInject 和@Assisted
现在你需要通知Dagger哪些类使用辅助注入,以及哪些参数应该由Dagger 提供。
打开 bitmap包下的 ImageLoader.kt,然后修改它的构造函数,就像下面这样:
// 1
class ImageLoader @AssistedInject constructor( // 2
private val bitmapFetcher: BitmapFetcher,
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher,
@Dispatchers.Main private val uiDispatcher: CoroutineDispatcher,
@Assisted
@DrawableRes private val loadingDrawableId: Int = R.drawable.loading_animation_drawable, // 3
@Assisted
private val imageFilter: ImageFilter = NoOpImageFilter // 3
) {
// ...
}
在上面的代码中,你:
- 删除了
@AutoFactory
,这个注解不需要了 - 用
@AssistedInject
标签对主构造函数注解. - 删除了
@Provided
注释并为你要提供的构造函数参数添加了@Assisted
注释。
请注意,你之前是同@Provided
标记 Dagger 提供的参数的。现在则相反:使用@Assisted
标记你要提供的参数。
如果你现在尝试构建并运行该项目,你会遇到一些错误。这是因为使用 Dagger 进行辅助注射还需要一个步骤才能完成。
使用 @AssistedFactory 创建Factory
告诉 Dagger 工厂方法应该是什么样子。
在 di包中,使用以下代码创建一个名为 ImageLoaderFactory.kt的新文件:
@AssistedFactory // 1
interface ImageLoaderFactory {
fun createImageLoader( // 2
@DrawableRes loadingDrawableId: Int = R.drawable.loading_animation_drawable,
imageFilter: ImageFilter = NoOpImageFilter
): ImageLoader // 3
}
在这部分代码中,你:
- 创建
ImageLoaderFactory
并用@AssistedFactory
对它注解。 - 定义
createImageLoader()
方法,方法包含你之前在ImageLoader
的构造函数中用@Assisted
注释的参数。请注意,你可以随意命名此方法 — 也可以将其称为create()。 - 指定
ImageLoader
为返回类型。
如果你现在构建应用程序,Hilt 注解处理器将生成ImageLoaderFactory
的代码。不过构建最终还是会失败,因为你仍然需要在 MainActivity中集成新代码。
在合适的地方使用辅助注射
就像你使用 AutoFactory 所做的那样,你现在可以将Hilt生成的ImageLoaderFactory
注入MainActivity
. 打开 MainActivity.kt并进行以下更改:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var imageLoaderFactory: ImageLoaderFactory // 1
// ...
fun loadImage() {
lifecycleScope.launch {
imageLoaderFactory
.createImageLoader( // 2
R.drawable.loading_animation_drawable,
GrayScaleImageFilter()
).loadImage(imageUrlStrategy(), mainImage)
}
}
}
在这部分代码中,你:
- 注入一个
ImageLoaderFactory
在这种情况下,你需要更新ImageLoaderFactory所在的包。现在它在 di包中。 - 使用你在接口中定义的新createImageLoader工厂方法。
构建并运行该应用程序,然后查看它是否可以正常运行。
看起来一切正常,但是在使用 AutoFactory 时作为限制的默认参数呢?
在 Dagger 辅助注入中使用默认参数
好消息是,使用 Dagger 辅助注入时,你依然可以使用可选参数。这是因为 Dagger 生成的代码是@AssistedFactory
注解的接口,它是一个 Kotlin 接口。打开MainActivity.kt并像这样更改它:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// ...
fun loadImage() {
lifecycleScope.launch {
imageLoaderFactory
.createImageLoader( imageFilter = GrayScaleImageFilter() // HERE
).loadImage(imageUrlStrategy(), mainImage)
}
}
}
如你所见,你传递了一个imageFilter
参数,同时在使用了loadingDrawableId
的默认值。
构建并运行应用程序以检查一切是否仍按预期工作。
辅助注射和ViewModels
辅助注入的一个常见用例是注入ViewModel
.
Google仍在这方面努力,你现在学到的内容将来可能会发生变化。要了解这是如何工作的,你将通过以下步骤将加载和变换一个Bitmap
的代码移动到一个ViewModel
:
- 添加一些必需的依赖项。
- 实现新的
ImageLoaderViewModel.
- 为
ImageLoaderViewModel
提供一个@AssistedFactory
。 - 在
MainActivity
使用ImageLoaderViewModel
是时候一起编码了。
添加所需的依赖项
打开应用模块的 build.gradle,并添加以下内容:
implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03" // 1
implementation "androidx.activity:activity-ktx:1.2.2" // 2
这些依赖项包括:
- Hilt对 ViewModel的支持。(译者注,在译者开发时用到的Hilt的2.37版中,不需要添加此依赖库,添加后编译会报错)
Activity上
的Kotlin拓展,它允许你使用viewModels()
获取ViewModel
.
现在你可以开始实现ImageLoaderViewModel
。
实现ViewModel
为了展示辅助注入如何与ViewModel
一起使用,
你需要创建ImageLoaderViewModel
,
它将实现与ImageLoader
相同的功能。
创建一个名为 viewmodels的新包,同时在其中创建一个名为 ImageLoaderState.kt的新文件
使用以下代码:
sealed class ImageLoaderState
data class LoadingState(@DrawableRes val drawableId: Int) : ImageLoaderState()
data class SuccessState(val bitmap: Bitmap) : ImageLoaderState()
这是一个密封类,代表你可以根据不同状态在ImageView
中放入的不同内容
一个Drawable
在你获取和转换图像时显示,一个Bitmap
作为结果显示。
在同一个包中,创建另一个名为 ImageLoaderViewModel.kt 的新文件并添加以下代码:
class ImageLoaderViewModel @AssistedInject constructor( // 1
private val bitmapFetcher: BitmapFetcher, // 2
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher, // 2
@Assisted private val imageFilter: ImageFilter, // 3
@Assisted private val loadingDrawableId: Int // 3
) : ViewModel() {
private val _bitmapLiveData = MutableLiveData<ImageLoaderState>()
val bitmapLiveData: LiveData<ImageLoaderState>
get() = _bitmapLiveData
fun loadImage(imageUrl: String) { // 4
viewModelScope.launch(bgDispatcher) {
_bitmapLiveData.postValue(LoadingState(loadingDrawableId))
val bitmap = bitmapFetcher.fetchImage(imageUrl)
val filteredBitmap = imageFilter.transform(bitmap)
_bitmapLiveData.postValue(SuccessState(filteredBitmap))
}
}
}
让我们一步一步地回顾一下刚才做的:
- 用
@AssistedInject
注释ImageLoaderViewModel
。理论上,你应该使用Hilt 提供的@HiltViewModel
处理ViewModel
,但不幸的是,这还不适用于辅助注入。(有关详细信息,请参阅此问题。) - 定义
bitmapFetcher
和bgDispatcher
作为主要构造函数参数,这两个参数由Dagger注入的。 - 使用
@Assisted
注释了imageFilter
和loadingDrawableId
,这两个参数在创建ImageLoaderViewModel
由你提供。 - 提供
loadImage()
方法的实现,包含了用于获取和转换bitmap,以及使用 LiveData更新ImageLoaderState
的逻辑代码
为 ViewModel 创建 @AssistedFactory
你需要告诉 Dagger 如何创建一个带有辅助注入的ImageLoaderViewModel
的实例。
在同一个viewmodels包中,
创建一个名为ImageLoaderViewModelFactory.kt的新文件,并编写以下代码:
class ImageLoaderViewModel @AssistedInject constructor( // 1
private val bitmapFetcher: BitmapFetcher, // 2
@Dispatchers.IO private val bgDispatcher: CoroutineDispatcher, // 2
@Assisted private val imageFilter: ImageFilter, // 3
@Assisted private val loadingDrawableId: Int // 3
) : ViewModel() {
private val _bitmapLiveData = MutableLiveData<ImageLoaderState>()
val bitmapLiveData: LiveData<ImageLoaderState>
get() = _bitmapLiveData
fun loadImage(imageUrl: String) { // 4
viewModelScope.launch(bgDispatcher) {
_bitmapLiveData.postValue(LoadingState(loadingDrawableId))
val bitmap = bitmapFetcher.fetchImage(imageUrl)
val filteredBitmap = imageFilter.transform(bitmap)
_bitmapLiveData.postValue(SuccessState(filteredBitmap))
}
}
}
这段代码现在应该很简单了。在这里,你:
- 创建
ImageLoaderViewModelFactory
,用@AssistedFactory
注解. - 定义
create()
方法,方法的参数也是你在ViewModel
的构造函数中用@Assisted
标记的参数
Dagger 将生成管理辅助注入的代码,但对于ViewModel
,你需要提供ViewModelProvider.Factory
.
在同一个ImageLoaderViewModelFactory.kt文件中,在文件添加以下顶级函数(译者注:顶级函数,Top level function, 是指不在类中,不属于任何对象,直接在文件中声明的函数):
fun provideFactory(
assistedFactory: ImageLoaderViewModelFactory, // 1
imageFilter: ImageFilter = NoOpImageFilter,
loadingDrawableId: Int = R.drawable.loading_animation_drawable
): ViewModelProvider.Factory =
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return assistedFactory.create(imageFilter, loadingDrawableId) as T // 2
}
}
在这部分代码中,你创建了provideFactory()
方法,
它将返回ViewModelProvider.Factory
的实现,
用于创建ImageLoaderViewModel
的实例。
请注意你要:
- 将
ImageLoaderViewModelFactory
作为参数传递。 - 使用
assistedFactory
创建ImageLoaderViewModel
的实例。
当你在MainActivity
中注入ImageLoaderViewModel
时,你就要用到provideFactory()
方法。
辅助注入ViewModel
现在是在MainActivity
中使用ImageLoaderViewModel
的时候了。打开MainActivity.kt,并像这样更改它:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject
lateinit var imageLoaderViewModelFactory: ImageLoaderViewModelFactory // 1
private val imageLoaderViewModel: ImageLoaderViewModel by viewModels { // 2
provideFactory( // 3
imageLoaderViewModelFactory, // 4
GrayScaleImageFilter()
)
}
@Inject
lateinit var imageUrlStrategy: ImageUrlStrategy
lateinit var mainImage: ImageView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
mainImage = findViewById<ImageView>(R.id.main_image).apply {
setOnLongClickListener {
loadImage()
true
}
}
imageLoaderViewModel.bitmapLiveData.observe(this) { event ->
with(mainImage) {
when (event) {
is LoadingState -> {
scaleType = ImageView.ScaleType.CENTER_INSIDE
setImageDrawable(ContextCompat.getDrawable(
this@MainActivity,
event.drawableId)
)
}
is SuccessState -> {
scaleType = ImageView.ScaleType.FIT_XY
setImageBitmap(event.bitmap)
}
}
}
}
}
override fun onStart() {
super.onStart()
loadImage()
}
fun loadImage() {
imageLoaderViewModel.loadImage(imageUrlStrategy())
}
}
在这部分代码中,你:
- 使用
@Inject
注入ImageLoaderViewModelFactory
。 - 使用
viewModels()
获得一个ImageLoaderViewModel
实例。 - 调用
provideFactory()
以获取ViewModelProvider.Factory
,然后创建ImageLoaderViewModel
的实例。这也是你可以使用默认值的地方。 - 将
ImageLoaderViewModelFactory
作为参数传递给provideFactory()
。这个工厂已经被Dagger 注入了依赖项,它可以将参数传递给它创建的ViewModel
。
最后一次构建并运行应用程序以测试一切是否按预期工作。
最后
如果你想查看 AssistedGallery 应用程序的最终版本,请单击本教程顶部或底部的“下载资料”按钮下载项目。(译者注,请到原文链接中下载)
很棒,现在你已经完成了教程!你已经了解了什么是辅助注入以及如何使用 AutoFactory 和Dagger/Hilt 2.31版来实现它。你还学习了如何对ViewModel
架构组件使用辅助注入。
要了解有关使用 Hilt 进行依赖注入的更多信息,请查看使用 Hilt 进行依赖注入(基础) 视频课程和Dagger教程一书。
我们希望你喜欢本教程。如果你有任何疑问或意见,请加入下面的论坛讨论!