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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Assisted Injection With Dagger and Hilt
30 mins
- Getting Started
- AssistedGallery App Architecture
- The ImageLoader Class
- Managing Dependencies With Constructor Injection
- Implementing the loadImage Function
- Using the ImageLoader Class
- Injecting Only What You Need
- What About the Other Parameters?
- Using Assisted Injection With AutoFactory
- Configuring AutoFactory
- Using AutoFactory in the AssistedGallery App
- Preparing a Class for Assisted Injection
- A Look at the Generated Code
- Using the Generated Factory
- Assisted Injection with Dagger 2.31+
- Updating the Dependencies
- Using @AssistedInject and @Assisted
- Creating a Factory with @AssistedFactory
- Assisted Injection on the Use Site
- Using Default Parameters With Dagger Assisted Injection
- Assisted Injection and ViewModels
- Adding the Required Dependencies
- Implementing the ViewModel
- Creating an @AssistedFactory for the ViewModel
- Assisted Injecting the ViewModel
- Where to Go From Here?
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:
- 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. - Use the new createImageLoader factory method you’ve defined in the interface.
Build and run the app and see that it works as expected.
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.
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
forImageLoaderViewModel
. - Use
ImageLoaderViewModel
inMainActivity
.
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:
- Hilt support for ViewModels.
- Kotlin extensions for
Activity
, which allows you to get aViewModel
usingviewModels()
.
Now you can start implementing ImageLoaderViewModel
.
Implementing the ViewModel
To show how assisted injection works with ViewModel
s, 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:
- Annotate
ImageLoaderViewModel
with@AssistedInject
. In theory, you should use the@HiltViewModel
that Hilt provides when dealing withViewModel
s, but unfortunately, this doesn’t yet work with assisted injection. (See this issue for details.) - Define
bitmapFetcher
andbgDispatcher
as primary constructor parameters that Dagger should inject. - Use
@Assisted
for theimageFilter
andloadingDrawableId
parameters that you’ll provide when creatingImageLoaderViewModel
. - Provide an implementation for
loadImage()
containing the logic for fetching and transforming the bitmap and updatingImageLoaderState
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:
- Create
ImageLoaderViewModelFactory
, which is annotated with@AssistedFactory
. - Define
create()
with the parameters you marked with@Assisted
in theViewModel
‘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:
- Pass
ImageLoaderViewModelFactory
as a parameter. - Use
assistedFactory
to create the instance ofImageLoaderViewModel
.
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:
- Inject
ImageLoaderViewModelFactory
using@Inject
. - Use
viewModels()
to get anImageLoaderViewModel
instance. - Invoke
provideFactory()
to get the reference to theViewModelProvider.Factory
that allows you to create the instance ofImageLoaderViewModel
. This is also where you could use default values. - Pass
ImageLoaderViewModelFactory
as a parameter toprovideFactory()
. This factory is already injected with dependencies by Dagger, which it can pass on to theViewModel
it will create.
Build and run the app one last time to test that everything is working as expected.