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 3 of 5 of this article. Click here to view the first page.

A Look at the Generated Code

To understand how to use ImageLoader, you need to build the app and look at the generated code in the build/generated/source/kapt/debug directory, as seen in the following picture:

AutoFactory Generated Code

Note: Switch to the Project view to see the build folders.

If you open ImageLoaderFactory.java, you’ll see the following:

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

This Java code generated by AutoFactory contains many interesting things:

  1. The @Generated annotations provide metadata about what generated the file.
  2. A final field for each constructor parameter you annotated with @Provided.These fields are initialized by using the constructor parameters of the factory.
  3. The @Inject annotation on the constructor means that Dagger will be able to create an instance of ImageLoaderFactory.
  4. AutoFactory generates create()with the parameters you didn’t mark as @Provided. The implementation is quite simple; it creates an ImageLoader instance using both the values from the constructor and the ones passed as parameters of create() itself.

It’s now time to use the generated ImageLoaderFactory in the MainActivity.

Using the Generated Factory

Open MainActivity.kt in the ui package and apply the following changes:

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

With this code, you:

  1. Inject the ImageLoaderFactory in place of the ImageLoader.
  2. Invoke create(), passing in a Drawable and an ImageFilter to create an instance of ImageLoader. In this example, you’re using a GrayScaleImageFilter as the ImageFilter implementation.
Note: As you’re now injecting ImageLoaderFactory, you can delete ImageLoaderModule.kt in the di package.

Build and run the app to see the new grayscale filter in action.

AssistedGallery with ImageFilter

This means Dagger provides some of the dependencies through Factory, and you provide the remaining dependencies as parameters for create().

Note: You might be thinking: The parameters for Drawable to display while loading and ImageFilter used to have default values. Where did those go? Java doesn’t have a concept of default values for parameters, so the annotation processor doesn’t know they exist. You might think that using @JvmOverloads would generate different overloads for create(), but unfortunately, this isn’t yet supported.

Assisted Injection with Dagger 2.31+

If you’re using Dagger with version 2.31 or later, you can benefit from assisted injection without any additional dependencies. As you’ll see very soon, you can achieve the same result you got with @AutoFactory by using different annotations.

To migrate to assisted injection with Dagger, you need to:

  1. Remove dependencies to AutoFactory and update the version of Dagger/Hilt.
  2. Use @AssistedInject and @Assisted instead of @AutoFactory and @Provided, respectively.
  3. Define a Factory implementation with the @AssistedFactory annotation.

It’s time to migrate ImageLoader to using assisted injection with Dagger.

Updating the Dependencies

As the first step, open build.gradle for the app module and remove the definitions you added earlier:

  // 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

After that, upgrade the version for Hilt. At the time of writing, this is 2.33-beta. You can also check MavenCentral for the latest available version.

To update the version for Hilt, change the value for hilt_android_version. Open the project-level build.gradle file and update the version:

buildscript {
  ext.kotlin_version = "1.4.31"
  ext.hilt_android_version = "2.33-beta" // Update this value
  repositories {
    google()
    mavenCentral()
  }
  // ...
}
// ...
Note: Don’t forget to sync your project with Gradle after making changes to the build.gradle files.

Before proceeding, open ApplicationModule.kt and replace both references to ApplicationComponent::class with SingletonComponent::class, as this was renamed in newer Dagger versions:

@Module(includes = arrayOf(Bindings::class))
@InstallIn(SingletonComponent::class) // Check this line
object ApplicationModule {
  // ...
  @Module
  @InstallIn(SingletonComponent::class) // Check this line
  interface Bindings {
    // ...
  }
}

Your code should look like what’s shown above.

Using @AssistedInject and @Assisted

Now you need to inform Dagger about the classes that use assisted injection, as well as about what parameters Dagger should provide. Open ImageLoader.kt in the bitmap package and change its constructor, like so:

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

With this code, you:

  1. Removed the @AutoFactory annotation, which is no longer needed.
  2. Annotated the primary constructor with @AssistedInject.
  3. Removed the @Provided annotations and added @Assisted annotations instead for the constructor parameters you’re going to provide.

Note how earlier, you used @Provided to mark the parameters provided by Dagger. Now you’re doing the opposite: using @Assisted for the parameters you’re going to provide.

If you try to build and run the project now, you’ll get some errors. This is because assisted injection with Dagger requires one more step to be complete.

Creating a Factory with @AssistedFactory

Tell Dagger what the factory method should look like. In the di package, create a new file called ImageLoaderFactory.kt with the following code:

@AssistedFactory // 1
interface ImageLoaderFactory {

  fun createImageLoader( // 2
    @DrawableRes loadingDrawableId: Int = R.drawable.loading_animation_drawable,
    imageFilter: ImageFilter = NoOpImageFilter
  ): ImageLoader // 3
}

In this code, you:

  1. Create ImageLoaderFactory and annotate it with @AssistedFactory.
  2. Define createImageLoader() with the parameters you previously set as @Assisted in the constructor of ImageLoader. Note that you can name this method freely — it could also be called create().
  3. Specify ImageLoader as the return type.

If you now build the app, the Hilt annotation processor will generate the code for the ImageLoaderFactory implementation. The build will fail though, as you still need to integrate the new code in the MainActivity.