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?
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:
- Inject the
ImageLoader
directly into theimageLoader
instance variable. - Use this
imageLoader
to load the image to display.
Build and run, and check that everything is still working as expected.
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:
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:
-
ImageLoaderFactory
has@Inject
on its primary constructor: Dagger needs to know how to create an instance of it. - The parameters Dagger has to provide are parameters of the primary constructor for
ImageLoaderFactory
. - Dependencies you provide are parameters of
create()
. -
create()
combines all the parameters to create an instance ofImageLoader
.
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:
- Add the implementation dependency to the annotations you’ll use in your code.
- Using kapt, set up the annotation processor that will generate the code for assisted injection.
- Add some annotations that the code generated by AutoFactory will use (for example,
@Generated
). You’ll usecompileOnly
, 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.
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:
- Annotate the class name with
@AutoFactory
so that AutoFactory will process it and generate code. - Use
@Provided
to annotate thebitmapFetcher
,bgDispatcher
anduiDispatcher
constructor parameters. This marks these as the ones Dagger will have to provide. -
Do not annotate
loadingDrawableId
andimageFilter
. These are the constructor parameters you’ll provide when creating theImageLoader
instance using the factory.