B.
Appendix B: Assisted Injection
Written by Massimo Carli
Dagger and Hilt are libraries in continuous evolution. Google and Square, with the help of the open-source community, keep improving them, both by creating new features and by improving the performance of the existing ones.
One of Dagger’s new improvements is assisted injection, which Google added in version 2.31. In this appendix, you’ll learn:
- What assisted injection is.
- How to implement it with
@Assisted,@AssistedInjectand@AssistedFactory.
To do this, you’ll work on the RandomFunNumber app.
What is assisted injection?
In this book, you learned all about dependency injection. You saw several examples of how dependency injection can improve the maintainability and testability of your code. You also learned that constructor injection is the best type of injection because it allows you to provide dependencies at the exact moment you create an instance of an object.
It’s not unusual to see code like this:
class MyService @Inject constructor( // 1
private val dep1: Dependency1, // 2
private val dep2: Dependency2, // 2
private val dep3: Dependency3 // 2
) : Service
Here, you define:
-
MyServiceas an implementation of theServiceinterface, and you annotate its primary constructor with@Inject. This tells Dagger how to create an instance ofMyServiceto use anywhere you need an object of typeService. - The dependencies of
MyServiceas primary constructor parameters. In this case,MyServiceneeds objects of typesDependency1,Dependency2andDependency3.
By also telling Dagger how to provide objects of type Dependency1, Dependency2 and Dependency3, you know Dagger will create the instance of MyService for you every time you need a Service.
This is very cool, but sometimes you need something a bit different. To understand what, return to RandomFunNumber to see a practical example.
An example of constructor injection
Open the RandomFunNumber project from the starter folder of the materials for this appendix. This is a simplified version of the project you used in Chapter 19, “Testing With Hilt”, with an important difference.
In versions.gradle, the version of the Hilt library is now 2.31.2-alpha. It depends on version 2.31.2 of Dagger:
hilt_version = "2.31.2-alpha"
This is important because assisted injection was introduced in Dagger 2.31, so you need to use at least that version.
Now, open FunNumberServiceImpl.kt in the business package and look at the following code:
class FunNumberServiceImpl @Inject constructor( // 1
private val numberGenerator: NumberGenerator, // 2
private val funNumberEndpoint: FunNumberEndpoint // 2
) : FunNumberService {
// ...
}
This is an example of constructor injection, where you:
- Use
@Injectto tell Dagger to use the primary constructor to create the instances. - Define the dependencies on
NumberGeneratorandFunNumberEndpoint.
Next, open ActivityModule.kt in di to find the following definition:
// ...
@Binds
@ActivityScoped
fun bindFunNumberService( // HERE
impl: FunNumberServiceImpl
): FunNumberService
// ...
This code just says that every time you need to inject an object of the type FunNumberService, Dagger will create an instance of FunNumberServiceImpl to handle the dependencies you saw earlier.
With this configuration, Dagger provides all the dependencies for you. But what if you want to provide a different dependency every time you need a FunNumberServiceImpl? That’s where assisted injection comes in handy.
Providing dependencies with assisted injection
Suppose you want to provide a different NumberGenerator implementation every time you need a FunNumberServiceImpl. One way to achieve this is to use a custom qualifier.
However, while that would work, it would be quite verbose. Assisted injection is an elegant alternative.
To use this, you need:
-
@AssistedInjectin place of@InjectforFunNumberServiceImpl. -
@Assistedto tell Dagger which dependency you’ll provide it. -
@AssistedFactoryto define the Factory that binds typeFunNumberService. - To inject the
Factorywherever you need aFunNumberServiceand provide the proper dependency.
Now, you’re ready to code along and add assisted injection to RandomFunNumber.
Replacing @Inject with @AssistedInject
For your first step, you’ll inform Dagger that you want to use assisted injection, and will therefore provide some of the dependencies you need for a specific binding.
To do this, open FunNumberServiceImpl.kt in business and apply the following change:
class FunNumberServiceImpl @AssistedInject constructor( // HERE
private val numberGenerator: NumberGenerator,
private val funNumberEndpoint: FunNumberEndpoint
) : FunNumberService {
// ...
}
This code replaces @Inject with @AssistedInject. This tells Dagger that you’ll explicitly provide some of the dependencies you define as primary constructor parameters.
However, the code above doesn’t tell Dagger which dependencies you’ll provide. For that, you need @Assisted.
Using @Assisted
You just learned how to tell Dagger that you’ll handle some of FunNumberServiceImpl’s dependencies. Now, you need to declare which dependencies you’ll provide.
In the same FunNumberServiceImpl.kt in business, add the following definition:
class FunNumberServiceImpl @AssistedInject constructor(
@Assisted private val numberGenerator: NumberGenerator, // HERE
private val funNumberEndpoint: FunNumberEndpoint
) : FunNumberService {
// ...
}
When you add the import for @Assisted, you’ll probably see two different options. That’s because there’s an @Assisted in the dagger.assisted package and another in androidx.hilt. The source code in the latter has a comment saying that it’ll be replaced with the former. Therefore, the one in dagger.assisted is the right one to import in your class.
In this code, you use @Assisted for the primary constructor parameter of type NumberGenerator. With this, you tell Dagger that, when it’s time to create the object to bind to FunNumberService, you’ll explicitly provide the dependency of type NumberGenerator.
But how can you do this? You need a Factory.
Using @AssistedFactory
So far, you’ve told Dagger that:
- You want to do part of its job by providing some of the dependencies for
FunNumberService’s binding. - You’ll provide the dependency for the parameter of type
NumberGenerator.
Now, you need a way to pass the binding: a Factory.
To add this, create a new file named FunNumberServiceFactory.kt in business and add the following code:
@AssistedFactory // 1
interface FunNumberServiceFactory { // 2
fun create(
numberGenerator: NumberGenerator // 3
): FunNumberServiceImpl // 4
}
In this code, you:
- Use
@AssistedFactoryto tell Dagger to generate this interface of theFactoryfor you and create the object to bindFunNumberServiceImpl. - Define
Factoryas an interface. - Need a
createoperation that has the@Assisteddependencies as its parameters. Earlier, you told Dagger that you’ll provide the dependency forNumberGenerator. This parameter is how you do that. - Return an object of type
FunNumberServiceImpl. This last point is fundamental. The returning type is not the type of theFunNumberServiceabstraction, but the type of@AssistedInject, which is its implementation.
Because of the last point, you can open ActivityModule.kt in di and change it to this:
@Module(includes = [
NavigationModule::class
])
@InstallIn(ActivityComponent::class)
object ActivityModule
In this code, you deleted the bindings for:
-
FunNumberService, because you’ll provide it throughFunNumberServiceFactory. -
NumberGeneratorbecause you’ll provide this information explicitly — as you’ll see in a bit.
Now that you’ve defined FunNumberServiceFactory, Dagger will also create the binding for you. This means you don’t need to add this definition to any @Module. You can now simply @Inject the FunNumberServiceFactory where you need it — in this case, in FunNumberViewModel.
Using FunNumberServiceFactory in FunNumberFragment
You now need to inject FunNumberServiceFactory where you need a FunNumberService.
Do this by opening FunNumberFragment.kt in ui.displaynumber and apply the following changes:
@AndroidEntryPoint
class FunNumberFragment : Fragment() {
private lateinit var funNumberTextView: TextView
private lateinit var funFactTextView: TextView
@Inject
lateinit var funNumberServiceFactory: FunNumberServiceFactory // 1
private lateinit var funNumberService: FunNumberService // 2
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val ctx = container?.context ?: IllegalStateException("Context not available")
funNumberService = funNumberServiceFactory.create(object : NumberGenerator { // 3
override fun randomNumber(): Int = 28
})
return LayoutInflater.from(ctx as Context).inflate(R.layout.fragment_show_number, container, false).apply {
// ...
}
}
override fun onStop() {
funNumberService.stop()
super.onStop()
}
}
In this code, you:
- Use
@Injectto get the reference toFunNumberServiceFactoryinto thefunNumberServiceFactoryproperty. - Define
funNumberServiceto store the reference to theFunNumberServiceinstance you’ll — theoretically — create later. - Invoke
create()on thefunNumberServiceFactoryto create theFunNumberServiceImplinstance, passing the reference to a newNumberGeneratorimplementation you’ll create locally.
Note: In this code, you’re only practicing how to use assisted injection. The instance of the
NumberGeneratorimplementation you create here doesn’t matter.
Now, run the app. Everything works as expected:
Limitations to assisted injection in Dagger
Congratulations! You’ve added assisted injection to the RandomFunNumber app. This is a new feature in Dagger, so be aware that following versions of the library might include improvements. At the moment, it has some limitations, including:
-
FunNumberServiceFactorydefinescreate(), which hasFunNumberServiceImplas return type. That’s because it must be the type of the class whose primary constructor you annotated with@AssistedInject. It would be nice to have theFunNumberServiceabstraction as the return type instead. - At the moment,
@AssistedInjectcannot use@Scopes.
Key points
- Dagger has offered assisted injection since version 2.31.0.
-
@AssistedInjectallows you to tag the primary constructor with@Assistedparameters. - To create an instance of
@AssistedInject, you need an@AssistedFactory.