9.
More About Modules
Written by Massimo Carli
In the previous chapter, you learned some important concepts about Dagger @Modules. You saw how to split the bindings for your app into multiple files using abstract classes, objects, companion objects and interfaces. You learned when and how to use the dagger.Lazy<T> interface to improve performance. And you used the Provider<T> interface to break cycled dependencies.
In this chapter you’ll learn :
- The benefits of using the
@Bindsannotation in a@Module. - How to provide existing objects. The Android
Contextis a classical example. - When optional bindings can help.
- How to provide different implementations of the same abstraction using qualifiers with the
@Namedannotation. - When to create custom qualifiers to make the code easier to read and less error-prone.
- How Android Studio can help you navigate the dependency tree.
As you can see, there’s still a lot to learn about @Modules.
Note: In this chapter, you’ll continue working on the RaySequence app but by the next one, you’ll have all the information you need to migrate the Busso App to Dagger.
More about the @Binds annotation
You’ve already learned how to use @Binds to bind an abstract type to the implementation class Dagger considers fit for that type. But @Binds has other benefits, as well. You’ll see proof of that next.
Open AppModule.kt and replace the existing code with the following:
@Module(includes = [AppBindings::class])
class AppModule {
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
FibonacciSequenceGenerator()
}
Here, you’re using FibonacciSequenceGenerator because it has a default constructor. You’ll see how to deal with the NaturalSequenceGenerator in a @Binds scenario later in the chapter.
Now,look at the di package in build/generated/source/kapt/debug, as shown in Figure 9.1:
As you see, there are two different files:
- AppModule_ProvideSequenceGeneratorFactory.kt, with 29 lines of code.
- DaggerAppComponent.kt, with 73 lines of code.
You also know that you can provide an instance of the SequenceGenerator<Int> implementation using @Binds. Replace the previous code with the following:
@Module(includes = [AppBindings::class])
interface AppModule {
@Binds
fun bindsSequenceGenerator(impl: FibonacciSequenceGenerator):
SequenceGenerator<Int>
}
Because you’re now delegating the creation of FibonacciSequenceGenerator to Dagger, you also need to change FibonacciSequenceGenerator.kt by adding the @Inject annotation, like so:
class FibonacciSequenceGenerator @Inject constructor() : SequenceGenerator<Int> {
// ...
}
Now, you can build again and check what’s in build/generated/source/kapt/debug, as shown in Figure 9.2:
As you can see, you now have just one file:
- DaggerAppComponent.kt with 62 lines of code.
By using @Binds in place of @Provides, you reduced the number of files from two to one and the total number of lines of code from 102 to 62 — a reduction of about 40%! This has a great impact on your work: Fewer classes and lines of code mean faster building time.
@Binds was added in Dagger 2.12 specifically to improve performance. So you might wonder, why not always use @Binds? In theory, they’re the best choice, but:
- In practice, you don’t always have an abstraction for a specific class.
- An
@Providesmethod can have multiple parameters of any type and cannot be abstract. A@Bindsfunction must be abstract and can only have one parameter that must be a realization of its return type. - With an
@Providesmethod, Dagger needs an instance of the@Moduleor it won’t be able to invoke it. - On the other hand, a
@Providercan have some logic that decides which implementation to use based on some parameter values.
These are all aspects you need to consider before choosing the best option for you.
Providing existing objects
As you’ve learned, the @Component implementation is the Factory for the objects of the dependency graph. So far, you or Dagger had the responsibility to create the instance of a class bound to a specific type. But what happens if the object you want to provide already exists? A practical example can help.
Open SequenceViewBinderImpl.kt in the view package and apply the following changes:
class SequenceViewBinderImpl @Inject constructor(
private var sequenceViewListener: SequenceViewBinder.Listener,
// 1
private val context: Context
) : SequenceViewBinder {
// ...
override fun showNextValue(nextValue: Int) {
// 2
output.text = context.getString(R.string.value_output_format, nextValue)
}
// ...
}
In this code, you:
- Add a second parameter to the primary constructor. This is the reference to the Android
Contextyou use inshowNextValue(). - Use the
contextto get access to a resource to format the output on the screen.
If you build the project now, you’ll get an error. This is expected because you’re delegating the creation of the instance of SequenceViewBinderImpl to Dagger, but you didn’t tell it how to create the Context. How can you do that? One option is to use @Module. But all your modules are interfaces now, and you need something more concrete.
Create a new file named ContextModule.kt in the di package and enter the following code:
@Module
class ContextModule(val context: Context) { // HERE
@Provides
fun provideContext(): Context = context
}
ContextModule is a class with a primary constructor that accepts a parameter with type Context. The object you pass into the primary constructor is the one you provide though provideContext(), annotated with @Provides. To use this @Module, you need to add it to the @Component module’s attribute values.
To do that, open AppComponent.kt and change it like this:
@Component(modules = [
AppModule::class,
ContextModule::class // HERE
])
@Singleton
interface AppComponent {
fun inject(mainActivity: MainActivity)
}
Build the app now and you’ll get the compilation error in Figure 9.3:
This happens because Dagger is smart enough to understand that it needs a Context to create the dependency graph. The code Dagger generates now is different.
To fix this, open MainActivity.kt and change the implementation of onCreate() like this:
class MainActivity : AppCompatActivity() {
// ...
override fun onCreate(savedInstanceState: Bundle?) {
DaggerAppComponent
.builder() // 1
.contextModule(ContextModule(this)) // 2
.build() // 3
.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewBinder.init(this)
}
// ...
}
Now, Dagger creates a DaggerAppComponent that contains a:
-
builder(), to get access to an implementation of the Builder Pattern for theAppComponentimplementation. -
contextModule()that hasContextModuleas a parameter type. Here, you create the instance ofContextModuleand pass the reference toMainActivity, which is theContextimplementation you need. -
build(), which is the one that the Builder Pattern defines to create theAppComponentimplementation instance.
Now, you can build and run and check that everything works as expected. You’ve added a small label before the current value for the sequence, as shown in Figure 9.4:
Note: In the next chapter, you’ll learn another way to do the same thing, by working on the
@Componentdefinition.
Adding Context this way is a common Android use case, but you could do the same with any other object.
Using optional bindings
What happens if a binding isn’t present? So far, you get a compilation error, but sometimes you need to make a binding optional.
Open SequenceViewBinderImpl.kt and apply the following changes:
@Singleton
class SequenceViewBinderImpl @Inject constructor(
// 1
private val context: Context
) : SequenceViewBinder {
// 1
@Inject
var sequenceViewListener: SequenceViewBinder.Listener? = null
override fun init(rootView: MainActivity) {
output = rootView.findViewById(R.id.sequence_output_textview)
rootView.findViewById<Button>(R.id.next_value_button).setOnClickListener {
sequenceViewListener?.onNextValuePressed() // 2
}
}
// ...
}
In this code, you:
- Move
sequenceViewListenerfrom being a primary constructor parameter to a normal optional property. Basically, you use field injection instead of constructor injection. - Because
sequenceViewListeneris now optional, you use the safe call operator:?.
Now, build and run. Hey, what’s happening? You’re getting an error like this:
error: [Dagger/InjectBinding] Dagger does not support injection into private fields
You already learned in the first section of the book that it isn’t possible to inject objects into private fields, but isn’t sequenceViewListener public by default?
Well, the property is, but the field is not. Dagger is a Java tool, remember? Look at the Java code for SequenceViewBinderImpl and you’ll see something like this:
public final class SequenceViewBinderImpl implements SequenceViewBinder {
@Inject
@Nullable
private Listener sequenceViewListener; // HERE
@Nullable
public final Listener getSequenceViewListener() {
return this.sequenceViewListener;
}
public final void setSequenceViewListener(@Nullable Listener var1) {
this.sequenceViewListener = var1;
}
// ...
}
The sequenceViewListener instance variable is private. The property is public because of getSequenceViewListener() and setSequenceViewListener().
Fortunately, the Kotlin language gives you a helping hand. Just change the definition of sequenceViewListener in SequenceViewBinderImpl.kt, like this:
@Singleton
class SequenceViewBinderImpl @Inject constructor(
private val context: Context
) : SequenceViewBinder {
@set:Inject // HERE
var sequenceViewListener: SequenceViewBinder.Listener? = null
// ...
}
By using the set: prefix, you’re telling the compiler to annotate the setter function of the property. The Java code for this is:
public final class SequenceViewBinderImpl implements SequenceViewBinder {
@Nullable
private Listener sequenceViewListener;
@Nullable
public final Listener getSequenceViewListener() {
return this.sequenceViewListener;
}
@Inject // HERE
public final void setSequenceViewListener(@Nullable Listener var1) {
this.sequenceViewListener = var1;
}
}
After the change, @Inject is on setSequenceViewListener(). In theory, this isn’t field injection, right? It looks more like method injection. But whatever it is, you can successfully build and run now.
Could you use the optional type, instead? Give it a try. Open AppBindings.kt and comment out — or simply remove — the binding for the SequenceViewBinder.Listener type like this:
@Module
abstract class AppBindings {
// ...
/*
@Binds
abstract fun bindViewBinderListener(impl: SequencePresenter):
SequenceViewBinder.Listener
*/
}
Build again and Dagger complains that:
SequenceViewBinder.Listener cannot be provided without an @Provides-annotated method.
The optional doesn’t work. The reason is still that Dagger is a Java tool, but there’s a solution: the Optional<T> type.
Note: Dagger supports the
Optional<T>type in the packagejava.utilin Android, but only from the API Level 24. The RaySequence app supports API Level 19, so you use theOptional<T>in the com.google.common.base package of https://github.com/google/guava, which you can already see in the dependencies in the build.gradle for the project.
Using @BindsOptionalOf
Open SequenceViewBinderImpl.kt and change the property definition like this:
@Singleton
class SequenceViewBinderImpl @Inject constructor(
private val context: Context
) : SequenceViewBinder {
@set:Inject
var sequenceViewListener: Optional<SequenceViewBinder.Listener> = Optional.absent() // 1
override fun init(rootView: MainActivity) {
output = rootView.findViewById(R.id.sequence_output_textview)
rootView.findViewById<Button>(R.id.next_value_button).setOnClickListener {
// 2
if (sequenceViewListener.isPresent) {
sequenceViewListener.get().onNextValuePressed()
}
}
}
// ...
}
In this code, you:
- Change the type of
sequenceViewListenertoOptional<SequenceViewBinder.Listener>with an initial value ofOptional.absent(). - Check if the value is present using
isPresent. Iftrue, you get its reference usingget().
Build the app now, and Dagger will complain again! That’s because it has no idea how to deal with Optional<T>. The problem is that Optional<T> is not a standard type, like the others. And that’s exactly why @BindsOptionalOf exists.
Open AppBindings.kt and add the following definition:
@Module
abstract class AppBindings {
@BindsOptionalOf // HERE
abstract fun provideSequenceViewBinderListener(): SequenceViewBinder.Listener
// ...
}
With the previous code, you’re telling Dagger that it might find an Optional<SequenceViewBinder.Listener> and that it shouldn’t complain if there isn’t a binding for it.
Now you can successfully build the app — but when you press the button, nothing happens. That’s because Optional<SequenceViewBinder.Listener>’s property sequenceViewListener has no bindings, so it’s Optional.absent().
To make the app work again, open AppBindings.kt and restore the following definition:
@Module
abstract class AppBindings {
// ...
@Binds
abstract fun bindViewBinderListener(impl: SequencePresenter):
SequenceViewBinder.Listener
}
Build and run now and everything works as expected. Optional<SequenceViewBinder.Listener> now has a value.
Using qualifiers
RaySequence contains two different model implementations of SequenceGenerator<T>:
NaturalSequenceGeneratorFibonacciSequenceGenerator
You bound each of these in AppModule.kt in different examples. For instance, you moved from the NaturalSequenceGenerator to the FibonacciSequenceGenerator because of the primary constructor parameter. It would be nice to have both implementations at the same time and to make it easier to change which one you use. However, if you just use both, Dagger will complain.
Open AppModule.kt and change it like this:
@Module(includes = [AppBindings::class])
interface AppModule {
@Binds
fun bindsNaturalSequenceGenerator(impl: NaturalSequenceGenerator): SequenceGenerator<Int>
@Binds
fun bindsFibonacciSequenceGenerator(impl: FibonacciSequenceGenerator):
SequenceGenerator<Int>
}
Build the app and you’ll get the following error:
error: [Dagger/DuplicateBindings] com...SequenceGenerator<java.lang.Integer> is bound multiple times:
Of course! You get this error because you have multiple bindings for the same abstraction. Dagger allows you to solve this problem with the concept of qualifiers.
Using the @Named annotation
The easiest way to solve the previous problem is by using the @Named annotation. To do that, just open AppModule.kt and add the following code:
// 1
const val NATURAL = "NaturalSequence"
const val FIBONACCI = "FibonacciSequence"
@Module(includes = [AppBindings::class])
interface AppModule {
@Binds
@Named(NATURAL) // 2
fun bindsNaturalSequenceGenerator(impl: NaturalSequenceGenerator): SequenceGenerator<Int>
@Binds
@Named(FIBONACCI) // 3
fun bindsFibonacciSequenceGenerator(impl: FibonacciSequenceGenerator):
SequenceGenerator<Int>
}
In this code, you:
- Define the
NATURALandFIBONACCIconstants you’ll use to identify the two differentSequenceGenerator<Int>implementations. - Use the
@Namedannotation with theNATURALconstant as its parameter to identify theNaturalSequenceGeneratorimplementation. - Do the same for the
FibonacciSequenceGeneratorimplementation using the@Namedannotation and theFIBONACCIparameter value.
Build now, but Dagger keeps complaining. Now it doesn’t know which one to use in SequencePresenterImpl.
Fix this by opening SequencePresenterImpl.kt and applying the following change:
@Singleton
class SequencePresenterImpl @Inject constructor(
) : BasePresenter<MainActivity,
SequenceViewBinder>(),
SequencePresenter {
@Inject
@Named(NATURAL) // HERE
lateinit var sequenceModel: SequenceGenerator<Int>
// ...
}
Using @Named, you’re telling Dagger that the instance you want to inject is the one with the NATURAL constant as the parameter of @Named. But now that you’re delegating creating the instance of NaturalSequenceGenerator to Dagger, it needs some small changes. Open NaturalSequenceGenerator.kt and add @Inject to the primary constructor, like this:
class NaturalSequenceGenerator @Inject constructor( // HERE
private var start: Int
) : SequenceGenerator<Int> {
override fun next(): Int = start++
}
But you’re not quite finished yet. Build the app now, and you’ll get the following error:
error: [Dagger/MissingBinding] java.lang.Integer cannot be provided without an @Inject constructor or an @Provides-annotated method.
Of course! Dagger doesn’t know what to pass as the value to the constructor parameter of type Int. But you already know how to fix this. Open AppModule.kt and add the following definitions:
const val NATURAL = "NaturalSequence"
const val FIBONACCI = "FibonacciSequence"
const val START_VALUE = "StartValue" // 1
@Module(includes = [AppBindings::class])
interface AppModule {
// 2
companion object {
@Provides
@JvmStatic
@Named(START_VALUE) // 3
fun provideStartValue(): Int = 0
}
}
In this code, you:
- Add a new
START_VALUEconstant. - Create a companion object, because Dagger doesn’t like to have concrete functions in an interface.
- Annotate
provideStartValue()with@NamedusingSTART_VALUE.
Now, you can finally edit NaturalSequenceGenerator.kt, adding @Named as a qualifier of the parameter you want to use for the primary constructor.
class NaturalSequenceGenerator @Inject constructor(
@Named(START_VALUE) private var start: Int
) : SequenceGenerator<Int> {
override fun next(): Int = start++
}
Now, you can finally successfully build and run and verify everything’s working as expected.
Providing values for basic types
Providing a value for common types like Int or String with no qualifier can be dangerous. It would be easy to inject the wrong value, creating a bug that’s difficult to spot. Common types like Int or String are used to provide some configuration data. In that case encapsulation can help.
Note: In Kotlin, there’s no concept of primitive types like in Java. In Kotlin, you have the concept of the following basic types:
Byte,Short,Int,Long,Float,Double,CharandBoolean. For the integers, you also have theUnsignedcounterparts:UByte,UShort,UIntandULong. Although aStringis not a primitive Java type, you can consider aStringa Kotlin basic type.
Create a new package named conf, add a new file named Config.kt to it and add the following code:
data class Config(
val startValue: Int
)
Then, change the code in AppModule.kt like this:
@Module(includes = [AppBindings::class])
interface AppModule {
// ...
companion object {
@Provides
@JvmStatic
fun provideConf(): Config = Config(0) // HERE
}
}
Here, you basically replace provideStartValue() with provideConf(), returning an instance of Config encapsulating all the configuration values you need. This allows you to remove @Named as well.
To complete the process, open NaturalSequenceGenerator.kt and apply the following changes:
class NaturalSequenceGenerator @Inject constructor(
config: Config // 1
) : SequenceGenerator<Int> {
private var start = config.startValue // 2
override fun next(): Int = start++
}
In this code, you:
- Inject the
Configinstance as the primary constructor parameter. - Initialize the
startvariable withConfig.startValue.
You changed NaturalSequenceGenerator, so you need to update the tests accordingly. Open NaturalSequenceGeneratorTest.kt in the test build type and change it like this:
class NaturalSequenceGeneratorTest {
@Test
fun `test natural sequence value`() {
val naturalSequenceIterator = NaturalSequenceGenerator(Config(0)) // HERE
listOf(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10).forEach {
assertEquals(it, naturalSequenceIterator.next())
}
}
@Test
fun `test natural sequence value starting in diffenet value`() {
val naturalSequenceIterator = NaturalSequenceGenerator(Config(10)) // HERE
listOf(10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20).forEach {
assertEquals(it, naturalSequenceIterator.next())
}
}
}
Now, you can successfully build and run!
Using custom qualifiers
In the previous section, you learned how to bind different definitions to the same type using @Named. To do this, you had to define some String constants to use in your code. Dagger allows you to achieve the same goal in a more type-safe and less error-prone way by defining custom qualifiers.
Create a new file named NaturalSequence.kt in the di package and add the following code:
@Qualifier // 1
@MustBeDocumented // 2
@Retention(AnnotationRetention.BINARY) // 3
annotation class NaturalSequence //4
These few lines of code are very important — they allow you to define an annotation named NaturalSequence.
In this code, you:
- Use
@Qualifierto identify this annotation as a way to qualify a specific binding, just like you did with@Named.@Qualifierisn’t a Dagger annotation, but rather another definition in the javax.inject package of the JSR-330. - Tag this annotation as something that’s part of a public API for a feature. Because of this, it needs a Javadoc.
- Set the retention of the annotation to
BINARY. This means that the annotation is stored in binary output, but invisible for reflection. - Finally, define
NaturalSequence, which has no attributes.
Next, create a new file in the di package named FibonacciSequence.kt and add the following code:
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.BINARY)
annotation class FibonacciSequence
The names are the only difference between @FibonacciSequence and @NaturalSequence.
Now, open AppModule.kt and change its content to the following:
@Module(includes = [AppBindings::class])
interface AppModule {
// ...
@Binds
@NaturalSequence // 1
fun bindsNaturalSequenceGenerator(impl: NaturalSequenceGenerator): SequenceGenerator<Int>
@Binds
@FibonacciSequence // 2
fun bindsFibonacciSequenceGenerator(impl: FibonacciSequenceGenerator):
SequenceGenerator<Int>
}
Here, you removed the NATURAL and FIBONACCI constants and replaced @Named with:
@NaturalSequence@FibonacciSequence
Next, open SequencePresenterImpl.kt and change the code like this:
@Singleton
class SequencePresenterImpl @Inject constructor(
) : BasePresenter<MainActivity,
SequenceViewBinder>(),
SequencePresenter {
@Inject
@NaturalSequence // HERE
lateinit var sequenceModel: SequenceGenerator<Int>
// ...
}
With this code, you also replaced @Named with your custom @NaturalSequence.
Now, you can build and run and check that everything works.
So what do custom qualifiers cost in terms of the code Dagger generates for you? Very little. Aside from the definitions and compilation of the source code for the annotations themselves, Dagger doesn’t generate any additional code. It just uses custom qualifiers to choose which specific implementation to inject. Nice job, Dagger!
Modules, bindings & Android Studio
While you were developing the example, you might have noticed some new icons in Android Studio, like the ones you see when you open AppModule.kt:
Since Android Studio 4.2, you can navigate the Dagger bindings directly from the code. There are two different icons that, when you click on them, tell you where you:
- Define the binding for the type.
- Use the type as a dependency.
It’s easy to see how this works.
Finding a type’s binding
You can find the source of a binding for a specific type by clicking the icon in Figure 9.6:
Try it on the icon numbered 3 — you end up in FibonacciSequenceGenerator.kt, which is the type bound to SequenceGenerator<Int>, which @Binds defines in AppModule.kt.
In Figure 9.7, you see that the second icon allows you to find where Dagger binds type.
Finding a type’s usage
In Figure 9.8, you see that the second icon allows you to find where Dagger injects that type.
Click on the icon in Figure 9.6 and you’ll return to AppModule.kt.
More interesting is what happens when you click the icon in Figure 9.8 in the @Component definition in AppComponent.kt: Android Studio shows what’s in Figure 9.9:
As a rule of thumb, you can use the icon in Figure 9.6 to go down in the dependency tree and the one in Figure 9.8 to go up.
Key points
- By using
@Binds, Dagger generates fewer and shorter files, making the build more efficient. - Dagger
@Modules allow you to provide existing objects, but you need to pass an instance of them to the@Componentbuilder that Dagger generates for you. -
@BindsOptionalOfallows you to have optional bindings. - You can provide different implementations for the same type by using
@Named. - Custom qualifiers allow you to provide different implementations for the same type in a type-safe way.
- From Android Studio 4.1, you can easily navigate the dependency graph from the editor.
Great job! With this chapter, you’ve completed Section Two of the book. You learned everything you need to know about Dagger @Modules and you experimented with using different fundamental annotations like @Binds, @Provides and BindsOptionalOf as well as useful interfaces like dagger.Lazy<T> and Provider<T>.
You also learned what qualifiers are, how to implement them with @Named and how to use custom @Qualifiers.
Now, it’s time to learn everything you need about @Components. See you in the next chapter!