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?
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.
This tutorial is part of a series about Dagger. If you’re not familiar with Dagger, take a look at these resources first:
- Dagger 2 Tutorial For Android: Advanced
- Dagger 2 Tutorial for Android: Advanced – Part 2
- Migrating from Dagger to Hilt
- The new Dagger by Tutorial book
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:
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:
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. :]
In this diagram, you see that:
-
ImageLoader
is a class that loads an image from a provided URL into anImageView
. -
ImageLoader
depends on aBitmapFetcher
implementation, which handles fetching theBitmap
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 twoCoroutineDispatcher
instances. - Finally, there’s an option to execute a
Bitmap
tranformation using different implementations of theImageFilter
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:
- Managing dependencies with constructor injection.
- 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:
-
ImageLoader
depends on an implementation ofBitmapFetcher
that it receives as its first constructor parameter. Like all the parameters, it’s a private, read-onlyval
. - 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 theBitmap
. The second is marked with@Dispatchers.Main
, and you’ll use it to interact with the UI. - The previous parameters were mandatory.
loadingDrawableId
is the first optional parameter that represents theDrawable
to display while the background job is in progress. - Finally, you have an optional
ImageFilter
parameter for the transformation you want to apply to theBitmap
you load from the network.
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:
- Use
withContext
to run the contained code in the context of the background thread. - Switch to the UI thread for setting the
Drawable
to display while loading and transforming theBitmap
. - In the context of the background thread, fetch the data for the
Bitmap
from the network. - Transform the
Bitmap
. As this is an expensive operation, you execute it in the context of the background thread. - 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:
- Use
@Dispatchers.IO
as the qualifier for injecting theCoroutineDispatcher
for the background thread. - Use
@Dispatchers.Main
as the qualifier for theCoroutineDispatcher
for the main thread. - Inject a
BitmapFetcher
. - Inject an
ImageUrlStrategy
that’s an object that creates the URL of the image to download. - Use all the dependencies to create an instance of
ImageLoader
and load the image intoImageView
.
This is definitely too much code, especially when using dependency injection. Do you really need to inject all those dependencies into MainActivity?