Home Android & Kotlin Tutorials

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.

4/5 2 Ratings

Version

  • Kotlin 1.4, Android 10.0, Android Studio 4.2

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.
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?

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.

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.

Assisted Injection on the Use Site

As you did with AutoFactory, you can now inject the ImageLoaderFactory generated by Hilt into MainActivity. Open MainActivity.kt and make the following changes:

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

In this code, you:

  1. Inject an ImageLoaderFactory. In this case, what you have to update is the package the type comes from. It’s now in the di package.
  2. Use the new createImageLoader factory method you’ve defined in the interface.

Build and run the app and see that it works as expected.

AssistedGalley using Dagger Assisted Injection

This is all good, but what about the default parameters that were a limitation when using AutoFactory?

Using Default Parameters With Dagger Assisted Injection

The good news when using assisted injection with Dagger is that you don’t lose the chance to have optional parameters. This is because the code Dagger generates is an implementation of the @AssistedFactory interface, which is a Kotlin interface. Open MainActivity.kt and change it like this:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
  // ...
  fun loadImage() {
    lifecycleScope.launch {
      imageLoaderFactory
          .createImageLoader( imageFilter = GrayScaleImageFilter() // HERE
          ).loadImage(imageUrlStrategy(), mainImage)
    }
  }
}

As you can see, you pass a value for imageFilter while using the default value for loadingDrawableId.

Build and run the app to check that everything is still working as expected.

Using Optional Parameters

Assisted Injection and ViewModels

A common use case for assisted injection is the injection of a ViewModel. Google is still working on this, and what you’ll learn here might change in the future. To see how this works, you’ll move the loading and transforming of a Bitmap into a ViewModel with the following steps:

  • Add some required dependencies.
  • Implement the new ImageLoaderViewModel.
  • Provide an @AssistedFactory for ImageLoaderViewModel.
  • Use ImageLoaderViewModel in MainActivity.

It’s time to code along.

Adding the Required Dependencies

Open build.gradle for the app module and add the following:

  implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03" // 1
  implementation "androidx.activity:activity-ktx:1.2.2" // 2

These are the dependencies for:

  1. Hilt support for ViewModels.
  2. Kotlin extensions for Activity, which allows you to get a ViewModel using viewModels().

Now you can start implementing ImageLoaderViewModel.

Implementing the ViewModel

To show how assisted injection works with ViewModels, you’ll create ImageLoaderViewModel, which will implement the same feature that ImageLoader did.

Create a new package called viewmodels — along with a new file with the name ImageLoaderState.kt in it — with the following code:

sealed class ImageLoaderState
data class LoadingState(@DrawableRes val drawableId: Int) : ImageLoaderState()
data class SuccessState(val bitmap: Bitmap) : ImageLoaderState()

This is a sealed class that represents the different contents you can put into an ImageView for different states: a Drawable to display while you’re fetching and transforming the image, and a Bitmap to display as a result.

In the same package, create another new file called ImageLoaderViewModel.kt and add the following code:

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

Let’s review what you’re doing, step by step:

  1. Annotate ImageLoaderViewModel with @AssistedInject. In theory, you should use the @HiltViewModel that Hilt provides when dealing with ViewModels, but unfortunately, this doesn’t yet work with assisted injection. (See this issue for details.)
  2. Define bitmapFetcher and bgDispatcher as primary constructor parameters that Dagger should inject.
  3. Use @Assisted for the imageFilter and loadingDrawableId parameters that you’ll provide when creatingImageLoaderViewModel.
  4. Provide an implementation for loadImage() containing the logic for fetching and transforming the bitmap and updating ImageLoaderState using LiveData

Creating an @AssistedFactory for the ViewModel

You need to tell Dagger how to create an instance of ImageLoaderViewModel with assisted injection. In the same viewmodels package, create a new file called ImageLoaderViewModelFactory.kt, and write the following code:

@AssistedFactory // 1
interface ImageLoaderViewModelFactory {

  fun create( // 2
    imageFilter: ImageFilter = NoOpImageFilter,
    loadingDrawableId: Int = R.drawable.loading_animation_drawable
  ): ImageLoaderViewModel
}

This code should be quite straightforward now. Here, you:

  1. Create ImageLoaderViewModelFactory, which is annotated with @AssistedFactory.
  2. Define create() with the parameters you marked with @Assisted in the ViewModel‘s constructor.

Dagger will generate the code to manage assisted injection, but for ViewModel, you need to provide an implementation of ViewModelProvider.Factory. In the same ImageLoaderViewModelFactory.kt file, add the following 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
    }
  }

In this code, you create provideFactory(), which will return the implementation of ViewModelProvider.Factory to use for the creation of the instance of ImageLoaderViewModel. Note how you:

  1. Pass ImageLoaderViewModelFactory as a parameter.
  2. Use assistedFactory to create the instance of ImageLoaderViewModel.

provideFactory() is what you’ll use when injecting ImageLoaderViewModel into MainActivity.

Assisted Injecting the ViewModel

Now it’s time to use ImageLoaderViewModel in MainActivity. Open MainActivity.kt, and change it like this:

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

In this code, you:

  1. Inject ImageLoaderViewModelFactory using @Inject.
  2. Use viewModels() to get an ImageLoaderViewModel instance.
  3. Invoke provideFactory() to get the reference to the ViewModelProvider.Factory that allows you to create the instance of ImageLoaderViewModel. This is also where you could use default values.
  4. Pass ImageLoaderViewModelFactory as a parameter to provideFactory(). This factory is already injected with dependencies by Dagger, which it can pass on to the ViewModel it will create.

Build and run the app one last time to test that everything is working as expected.

Assisted Injection and ViewModel

Where to Go From Here?

If you want to see the final version of the AssistedGallery app, download the project by clicking the Download Materials button at the top or bottom of this tutorial.

Great job completing the tutorial! You learned what assisted injection is and how to implement it using AutoFactory and the Dagger/Hilt implementation added in version 2.31. You also learned how to use assisted injection with the ViewModel architecture component.

To learn more about dependency injection with Hilt, check out the Dependency Injection with Hilt: Fundamentals video course, and the Dagger By Tutorials book.

We hope you enjoyed this tutorial. If you have any questions or comments, please join the forum discussion below!

Average Rating

4/5

Add a rating for this content

2 ratings

More like this

Contributors

Comments