Assisted Injection With Dagger and Hilt

Learn what assisted injection is used for, how it works, and how you can add it to your app with Dagger’s new built-in support for the feature. By Massimo Carli.

Leave a rating/review
Download materials
Save for later
Share

Dependency injection with Dagger is a hot topic in the Android community. Dagger and its new Hilt extension are both open source projects in continuous evolution, with new features and improvements coming almost every day. One of these new features is assisted injection, and it’s available from version 2.31.

In this tutorial, you’ll learn:

  • What assisted injection is and why it can be useful.
  • How to use assisted injection with versions of Dagger before 2.31 with AutoFactory.
  • The way assisted injection works with Dagger 2.31+.
  • How to use assisted injection with Hilt and ViewModels.

This tutorial is part of a series about Dagger. If you’re not familiar with Dagger, take a look at these resources first:

Note: This tutorial assumes you’re familiar with Android development and Android Studio. If these topics are new to you, read the Beginning Android Development and Kotlin for Android tutorials first.

This tutorial is part of a series about Dagger. If you’re not familiar with Dagger, take a look at these resources first:

Now, it’s time to dive in!

Getting Started

Download the starter version of the project by clicking the Download Materials button at the top or bottom of this tutorial. When you open the project in Android Studio, you’ll get the following source tree:

Assisted Gallery Starter Source Tree

This is the structure of the AssistedGallery project you’ll use for learning assisted injection. Build and run the app to see how it works. You’ll get something like the following:

The starter AssistedGallery App

Note: In your case, the image will probably be different. This is because the app uses the placeimg.com service, which provides a simple API for getting a random image based on a given dimension and topic.

Now that this is set up, it’s time to take a look at the app’s architecture.

AssistedGallery App Architecture

AssistedGallery is a simple app that implements and uses an ImageLoader. Before diving into the code, look at the following class diagram that describes the dependencies between the different components. Understanding the dependencies between the main components for the app is fundamental when you’re talking about dependency injection. :]

ImageLoader Class Diagram

In this diagram, you see that:

  • ImageLoader is a class that loads an image from a provided URL into an ImageView.
  • ImageLoader depends on a BitmapFetcher implementation, which handles fetching the Bitmap data from the network using a provided URL. How this is implemented isn’t important for this tutorial.
  • Accessing the network and other IO intensive operations is something you must do on a background thread, so ImageLoader depends on two CoroutineDispatcher instances.
  • Finally, there’s an option to execute a Bitmap tranformation using different implementations of the ImageFilter interface. The specific implementations of these filters aren’t important.

Read on to see how to represent this in code.

The ImageLoader Class

To understand how ImageLoader works, open ImageLoader.kt in the bitmap package and look at the code, which has two main parts:

  1. Managing dependencies with constructor injection.
  2. Implementing the loadImage function.

The previous class diagram is useful to see how to implement a constructor injection.

Managing Dependencies With Constructor Injection

Constructor injection is a great way to inject dependencies in a class, because this happens when you create the instance, making it immutable. For example, look at ImageLoader‘s primary constructor:

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

The code above contains many noteworthy things:

  1. ImageLoader depends on an implementation of BitmapFetcher that it receives as its first constructor parameter. Like all the parameters, it’s a private, read-only val.
  2. You need two different CoroutineDispatcher implementationss. The first is annotated with @Dispatchers.IO, and you’ll use it for background operations like accessing the network or transforming the Bitmap. The second is marked with @Dispatchers.Main, and you’ll use it to interact with the UI.
  3. The previous parameters were mandatory. loadingDrawableId is the first optional parameter that represents the Drawable to display while the background job is in progress.
  4. Finally, you have an optional ImageFilter parameter for the transformation you want to apply to the Bitmap you load from the network.
Note: Optional parameter here means that you don’t always have to provide an argument, because it has a default value.

Implementing the loadImage Function

Although it’s not necessary for dependency injection, for completeness, it’s useful to look at the implementation for 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)
          }
        }
      }
}

In this code, you:

  1. Use withContext to run the contained code in the context of the background thread.
  2. Switch to the UI thread for setting the Drawable to display while loading and transforming the Bitmap.
  3. In the context of the background thread, fetch the data for the Bitmap from the network.
  4. Transform the Bitmap. As this is an expensive operation, you execute it in the context of the background thread.
  5. Return to the UI thread to display the Bitmap.

Now, how can you provide all the dependencies that ImageLoader needs and use it?

Using the ImageLoader Class

Open MainActivity.kt in the ui package for the app, and look at the code there:

@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)
    }
  }
}

Here, you can see that you:

  1. Use @Dispatchers.IO as the qualifier for injecting the CoroutineDispatcher for the background thread.
  2. Use @Dispatchers.Main as the qualifier for the CoroutineDispatcher for the main thread.
  3. Inject a BitmapFetcher.
  4. Inject an ImageUrlStrategy that’s an object that creates the URL of the image to download.
  5. Use all the dependencies to create an instance of ImageLoader and load the image into ImageView.

This is definitely too much code, especially when using dependency injection. Do you really need to inject all those dependencies into MainActivity?