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
You are currently viewing page 2 of 5 of this article. Click here to view the first page.

Injecting Only What You Need

To streamline the code, you don’t need to inject all the dependencies the ImageLoader needs into MainActivity. Instead, you can inject the ImageLoader itself, asking Dagger to do the hard part.

Create a new file called ImageLoaderModule.kt in the di package, and write the following code:

@Module
@InstallIn(ActivityComponent::class)
object ImageLoaderModule {

  @Provides
  fun provideImageLoader(
      @Dispatchers.IO bgDispatcher: CoroutineDispatcher,
      @Dispatchers.Main mainDispatcher: CoroutineDispatcher,
      bitmapFetcher: BitmapFetcher
  ): ImageLoader = ImageLoader(
      bitmapFetcher,
      bgDispatcher,
      mainDispatcher
  )
}

In this code, you add an instance of ImageLoader to the dependency graph for the activity scope. This allows you to update the code in MainActivity.kt to the following:

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

As you can see, now you:

  1. Inject the ImageLoader directly into the imageLoader instance variable.
  2. Use this imageLoader to load the image to display.

Build and run, and check that everything is still working as expected.

The AssistedGaller App again

Note: Again, you’ll see a random image that the API provides.

What About the Other Parameters?

So far so good but… there’s a but. :] ImageLoader also has two optional parameters. How can you pass a value for loadingDrawableId and imageFilter if you want to inject ImageLoader as you just did?

One possible solution is to make loadingDrawableId and imageFilter parameters of loadImage, like this:

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

This is a perfectly viable solution, but it’s not something that makes sense with dependency injection. This is because you have to pass in Drawable and ImageFilter every time you want to load a new image. A better approach would be to pass them in just once when ImageLoader is created.

You want to create an instance of ImageLoader using some parameters that Dagger manages for you, and using some that you pass in yourself when you create the instance. This is assisted injection, which Dagger supports natively from version 2.31. However, many codebases aren’t on the latest version, so you’ll first see how you can use assisted injection with earlier versions of Dagger.

Using Assisted Injection With AutoFactory

Before Dagger 2.31, you can achieve assisted injection using AutoFactory, a code generator created for Java. It also works for Kotlin with some limitations.

Before looking at the code, it’s worth understanding what AutoFactory and other tools for assisted injection do. Suppose you have a class with some dependencies exactly like the ImageLoader from before:

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

This class has five primary constructor parameters. As you saw earlier, Dagger can provide instances for the first three of them. This means that if you need to create an instance of ImageLoader, you just need to provide values for the last two parameters. How can you do that? This is where Factory Method comes into play. Instead of injecting the whole ImageLoader, you might inject a Factory, like this:

ImageLoaderFactory

Now, translate this to code:

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

}

In this code, you see that:

  1. ImageLoaderFactory has @Inject on its primary constructor: Dagger needs to know how to create an instance of it.
  2. The parameters Dagger has to provide are parameters of the primary constructor for ImageLoaderFactory.
  3. Dependencies you provide are parameters of create().
  4. create() combines all the parameters to create an instance of ImageLoader.

Now, you’ve specified which parameters are provided by Dagger and which ones you’ll pass in when calling create(). Based on this information, AutoFactory will generate the code for the Factory for you.

Configuring AutoFactory

AutoFactory uses annotation processing to generate code. Open build.gradle in app and add the following lines to the dependencies block:

  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

In this code, you:

  1. Add the implementation dependency to the annotations you’ll use in your code.
  2. Using kapt, set up the annotation processor that will generate the code for assisted injection.
  3. Add some annotations that the code generated by AutoFactory will use (for example, @Generated). You’ll use compileOnly, as these are only needed during compilation.

In the same build.gradle file, add the following definition above the dependencies block:

kapt {
    correctErrorTypes = true
}

This enables error type inferring in stubs. This is useful because the AutoFactory annotation processor relies on precise types in declaration signatures. Without this definition, Kapt would replace every unknown type with NonExistentClass, making debugging very difficult when something is wrong during code generation.

Using AutoFactory in the AssistedGallery App

Once you’ve added the dependencies to your build.gradle file in app, the following annotations are available in your project:

  • @AutoFactory: Marks the type you want to provide using assisted injection.
  • @Provided: Marks the parameters that will be provided by Dagger to create the instance.
Note: Don’t forget, of course, to select Sync Project with Gradle files from the File menu in Android Studio to do exactly what that option says. :]

Preparing a Class for Assisted Injection

Using AutoFactory in the ImageLoader is a straightforward task. Open ImageLoader.kt in the bitmap package and change its header like this, keeping the existing implementation:

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

In this code, you:

  1. Annotate the class name with @AutoFactory so that AutoFactory will process it and generate code.
  2. Use @Provided to annotate the bitmapFetcher, bgDispatcher and uiDispatcher constructor parameters. This marks these as the ones Dagger will have to provide.
  3. Do not annotate loadingDrawableId and imageFilter. These are the constructor parameters you’ll provide when creating the ImageLoader instance using the factory.