【译文】使用Dagger和Hilt辅助注入

本文详细介绍了如何使用Dagger和Hilt进行辅助注入,包括Assisted Gallery应用的架构、ImageLoader类的实现、辅助注入的工作原理以及如何在Dagger 2.31+和Hilt中使用辅助注入。文章还涵盖了在ViewModels中使用辅助注入的方法,提供了一个逐步的学习指南。
摘要由CSDN通过智能技术生成

本文是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
) {
  // ...
}

上面的代码有很多值得注意的地方:

  1. ImageLoader依赖一个BitmapFetcher接口的实现类,作为它构造函数第一个接收的参数。像所有参数一样,它是private val,私有只读(译者注:非强制).
  2. 你需要两种不同的CoroutineDispatcher实现。第一个用@Dispatchers.IO注释,你可以使用它进行后台操作,例如访问网络或转换Bitmap. 第二个用@Dispatchers.Main注释,你可以使用它与 UI 进行交互。
  3. 前面的参数都是必传参数,loadingDrawableId是第一个可选参数,代表后台作业正在进行时要显示的Drawable
  4. 最后,还有一个可选的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)
          }
        }
      }
}

在这部分代码中,你可以:

  1. 使用withContext在后台线程的上下文中运行包含的代码。

  2. 切换到 UI 线程以在加载和转换Bitmap时设置对应的Drawble.

  3. 在后台线程的上下文中,从网络获取Bitmap的数据。

  4. 变换Bitmap. 由于这是一项开销很大的操作,因此需要在后台线程的上下文中执行它。

  5. 返回到 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)
    }
  }
}

在代码中,可以看到

  1. 使用了@Dispatchers.IO作为限定符,用于注入后台线程的CoroutineDispatcher
  2. 使用@Dispatchers.Main作为限定符,修饰主线程的CoroutineDispatcher
  3. 注入一个BitmapFetcher.
  4. 注入一个ImageUrlStrategy对象,它创建要下载图像的 URL。
  5. 使用所有依赖项创建一个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
    }
  }
}

如你所见,在新的代码中:

  1. ImageLoader直接注入到imageLoader实例变量中。
  2. 使用imageLoader来加载要显示的图像。

构建并运行,然后检查一切是否仍按预期工作。

在这里插入图片描述

注意:和之前一样,你会看到API提供的随机图像。

其他参数呢?

到目前为止一切都很好但是……是的,还有一个但是。:] ImageLoader还有两个可选参数。如果想要ImageLoader像刚才那样注入,如何传递loadingDrawableIdimageFilter的值呢?

一种解决方案是让loadImage中的参数loadingDrawableIdimageFilter如下所示:

suspend fun loadImage(
    imageUrl: String, 
    into: ImageView, 
    @DrawableRes loadingDrawableId: Int = R.drawable.loading_animation_drawable,
    imageFilter: ImageFilter = NoOpImageFilter) { /*... */ }

这是一个完全可行的解决方案,但它对依赖注入没有意义。这是因为(译者注:这种场景下这两个参数不应该是固定的)你需要在每次加载新图片时传递DrawableImageFilter参数。更好的方法是在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

}

在这部分代码中,可以看到:

  1. ImageLoaderFactory的主构造函数有一个@Inject注解:Dagger需要知道如何创建一个它的实例。
  2. Dagger需要提供的参数,是ImageLoaderFactory的主构造函数的参数。
  3. 你需要提供的依赖项是create()的参数.
  4. 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

在这部分代码中,你:

  1. 添加了代码中用到的注解所在的依赖项。
  2. 使用 kapt,设置注释处理器,该处理器将生成用于辅助注入的代码。
  3. 添加一些 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
) {
  // ...
}

在这部代码中。你可以:

  1. @AutoFactory注释类名,以便 AutoFactory 处理它并生成代码。
  2. 使用@Provided注释bitmapFetcherbgDispatcheruiDispatcher这三个构造函数的参数。这样他们就被标记为 Dagger 需要提供的参数。
  3. 不要注释 loadingDrawableIdimageFilter。这些是你在使用工厂创建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 代码包含很多有趣的东西:

  1. @Generated注释提供关于所生成的文件的元数据。

  2. 每个使用@Provided注释的构造函数参数的都用final字段修饰,使用工厂的构造函数时初始化这些参数。

  3. 构造函数上@Inject注解意味着Dagger将创建ImageLoaderFactory实例。

  4. 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)
    }
  }
}

在这部分代码中,你:

  1. 注入ImageLoaderFactory代替ImageLoader
  2. 调用create(),传入 DrawableImageFilter以创建 ImageLoader的实例。在这个例子中,你使用 GrayScaleImageFilter作为 ImageFilter实现。

注意:由于你现在注入了ImageLoaderFactory,就可以删除 di包中的 ImageLoaderModule.kt

构建并运行应用程序以查看新的灰度过滤器的运行情况。
在这里插入图片描述

这意味着 Dagger 通过 Factory提供了一些依赖项,你提供剩余的依赖项作为create()的参数.

注意:你可能会想:Drawable加载时显示的参数ImageFilter曾经有默认值。那些去了哪里?Java 没有参数默认值的概念,因此注解处理器不知道它们存在。你可能认为使用@JvmOverloads会为 生成不同的create()重载方法,但不幸的是,目前尚不支持。

Dagger2.31+的辅助注射

如果你使用Dagger2.31或更高的版本,则可以从辅助注入中受益,而无需任何其他依赖项。
正如你接下来会看到的,你可以通过使用不同的注释来获得与 @AutoFactory相同的结果。

要迁移使用Dagger的 辅助注射,你需要:

  1. 删除 AutoFactory 的依赖并更新 Dagger/Hilt 的版本。
  2. 分别使用@AssistedInject@Assisted代替@AutoFactory@Provided
  3. 定义一个 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
) {
  // ...
}

在上面的代码中,你:

  1. 删除了@AutoFactory,这个注解不需要了
  2. @AssistedInject标签对主构造函数注解.
  3. 删除了@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
}

在这部分代码中,你:

  1. 创建ImageLoaderFactory并用@AssistedFactory对它注解。
  2. 定义createImageLoader()方法,方法包含你之前在ImageLoader的构造函数中用@Assisted注释的参数。请注意,你可以随意命名此方法 — 也可以将其称为create()。
  3. 指定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)
    }
  }
}

在这部分代码中,你:

  1. 注入一个ImageLoaderFactory在这种情况下,你需要更新ImageLoaderFactory所在的包。现在它在 di包中。
  2. 使用你在接口中定义的新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

这些依赖项包括:

  1. Hilt对 ViewModel的支持。(译者注,在译者开发时用到的Hilt的2.37版中,不需要添加此依赖库,添加后编译会报错)
  2. 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))
        }
    }
}

让我们一步一步地回顾一下刚才做的:

  1. @AssistedInject注释ImageLoaderViewModel。理论上,你应该使用Hilt 提供的@HiltViewModel处理ViewModel,但不幸的是,这还不适用于辅助注入。(有关详细信息,请参阅此问题。)
  2. 定义bitmapFetcherbgDispatcher作为主要构造函数参数,这两个参数由Dagger注入的。
  3. 使用@Assisted注释了imageFilterloadingDrawableId,这两个参数在创建ImageLoaderViewModel由你提供。
  4. 提供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))
        }
    }
}

这段代码现在应该很简单了。在这里,你:

  1. 创建ImageLoaderViewModelFactory,用@AssistedFactory注解.
  2. 定义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的实例。

请注意你要:

  1. ImageLoaderViewModelFactory作为参数传递。
  2. 使用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())
  }
}

在这部分代码中,你:

  1. 使用@Inject注入ImageLoaderViewModelFactory
  2. 使用viewModels()获得一个ImageLoaderViewModel实例。
  3. 调用provideFactory()以获取ViewModelProvider.Factory,然后创建ImageLoaderViewModel的实例。这也是你可以使用默认值的地方。
  4. ImageLoaderViewModelFactory作为参数传递给provideFactory() 。这个工厂已经被Dagger 注入了依赖项,它可以将参数传递给它创建的ViewModel

最后一次构建并运行应用程序以测试一切是否按预期工作。
在这里插入图片描述

最后

如果你想查看 AssistedGallery 应用程序的最终版本,请单击本教程顶部或底部的“下载资料”按钮下载项目。(译者注,请到原文链接中下载)

很棒,现在你已经完成了教程!你已经了解了什么是辅助注入以及如何使用 AutoFactory 和Dagger/Hilt 2.31版来实现它。你还学习了如何对ViewModel架构组件使用辅助注入。

要了解有关使用 Hilt 进行依赖注入的更多信息,请查看使用 Hilt 进行依赖注入(基础) 视频课程和Dagger教程一书。

我们希望你喜欢本教程。如果你有任何疑问或意见,请加入下面的论坛讨论!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值