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

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