8.
Working With Modules
Written by Massimo Carli
In previous chapters, you’ve used @Modules as a way to give Dagger information it can’t get from the code itself. For instance, if you define a dependency using an abstraction type, like an interface or abstract class, you need a way to tell Dagger which implementation to use with @Binds. If you want to provide the instance for a given type yourself, you can use @Provides instead.
But you can use @Modules for more than that. As the name implies, they’re also a way to group definitions. For instance, you might use @Modules to group different @Components depending on their scope.
Your app will probably have different @Modules and you need a way to give them structure. @Modules can have dependencies as well.
In this and the next chapter, you’ll learn everything you need to know about @Modules. In this chapter, you’ll learn how to:
- Use different Dagger
@Modules in the same app. -
Optimize start-up performances using Dagger’s
Lazy<T>interface. - Avoid cycled dependencies using the
Providerinterface. - Use optional bindings.
As you see, there’s a lot to learn about @Modules.
Note: In this chapter, you’ll continue working on the RaySequence app. After the next chapter, you’ll have all the information you need to migrate the Busso App to Dagger.
Throughout the chapter, you’ll change configurations often. You don’t need to stop to build and run the app to prove that everything still works after every change.
Why use modules?
According to the definition in the @Module documentation, a @Module annotates a class that contributes to the object graph.
Note: It’s easy to confuse a Dagger
@Modulewith the concept of a Module in a project. You’ll see a note like this when there’s possible ambiguity.
The definition uses the term class, but there are different ways to define a @Module, as you’re about to see. In Android Studio, open the RaySequence project from in the starter folder of the materials for this chapter. Now, open AppModule.kt in the di package and look at the following code:
@Module
object AppModule {
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
@Module
interface Bindings {
@Binds
fun bindSequenceViewBinder(impl: SequenceViewBinderImpl): SequenceViewBinder
@Binds
fun bindSequencePresenter(impl: SequencePresenterImpl): SequencePresenter
@Binds
fun bindViewBinderListener(impl: SequencePresenter):
SequenceViewBinder.Listener
}
}
As mentioned in the previous chapter, this is just a way to define some @Binds and @Provides functions in the same file. These are actually two different modules. You can prove this by opening AppComponent.kt in the same di package:
@Component(
modules = [
AppModule::class,
AppModule.Bindings::class
]
)
@Singleton
interface AppComponent {
fun inject(mainActivity: MainActivity)
}
The modules attribute for the @Component annotation accepts an array of KClass<*>. In the previous code, AppModule and AppModule.Bindings are related, giving you a simple way to improve the code. In AppModule.kt, replace AppModule’s header with this:
@Module(includes = [AppModule.Bindings::class]) // HERE
object AppModule {
// ...
}
@Module has an includes attribute that allows you to do what the name says: Including AppModule.Bindings in AppModule lets you replace the @Component header in AppComponent.kt with this:
@Component(modules = [AppModule::class]) // HERE
@Singleton
interface AppComponent {
// ...
}
This is a small step that helps organize the code in your project.
Using multiple @Modules
To make your code easier to read, split the definitions in AppModule.kt into two. Create a new file named AppBindings.kt in the di package and add the following code:
@Module
interface AppBindings {
@Binds
fun bindSequenceViewBinder(impl: SequenceViewBinderImpl): SequenceViewBinder
@Binds
fun bindSequencePresenter(impl: SequencePresenterImpl): SequencePresenter
@Binds
fun bindViewBinderListener(impl: SequencePresenter):
SequenceViewBinder.Listener
}
Now, open AppModule.kt and replace its code with the following:
@Module(includes = [AppBindings::class]) // HERE
object AppModule {
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
Here, you removed the internal Bindings and updated the value for includes. This isn’t a big deal, but it allows you to explore other ways of defining a @Module.
Using an abstract class
AppBindings.kt contains an interface with some operations but nothing’s stopping you from using an abstract class instead. To do so, change the AppBindings code like this:
@Module
abstract class AppBindings {
@Binds
abstract fun bindSequenceViewBinder(impl: SequenceViewBinderImpl): SequenceViewBinder
@Binds
abstract fun bindSequencePresenter(impl: SequencePresenterImpl): SequencePresenter
@Binds
abstract fun bindViewBinderListener(impl: SequencePresenter):
SequenceViewBinder.Listener
}
Now the class is abstract, like @Binds are. So what influence does that have on the code Dagger generates? None at all.
It’s easy to see that the code is the same. Just check what’s in build/generated/source/kapt/debug in the app module for the two cases:
Here, you can see how the generated code hasn’t changed. That’s because you’ve just used two different ways of telling Dagger the same thing.
Using a concrete class
What about AppModule? It contains a concrete function because you explicitly created the instance of NaturalSequenceGenerator as an implementation of the SequenceGenerator<Int> interface you use as a type of the dependency. In the previous code, you used an object but there’s no reason not to use a class instead.
Open AppModule.kt and replace object with class, like this:
@Module(includes = [AppBindings::class])
class AppModule { // HERE
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
Build and run to see that everything’s fine but, this time, the code Dagger generated is different. Remember that, at the moment, Dagger works in a Java environment so what really matters is the Java equivalent of the code you write in Kotlin. Look at build/generated/source/kapt/debug in the app module again, and you’ll see that Dagger generates a file for each @Provides in the @Module.
In this case, you only needed to provide provideSequenceGenerator() for Dagger to generate AppModule_ProvideSequenceGeneratorFactory.kt. The content of this file changes depending on whether you define the @Module with a class or with an object.
The difference is that the object is basically a singleton: You already have one single instance. With the class, you need to create at least one instance — but you could create many.
Open AppModule.kt with Android Studio and select Tools ▸ Kotlin ▸ Show Kotlin Bytecode, as in Figure 8.3:
A window like the one in Figure 8.4 will appear on the right side of Android Studio.
You can ignore that bytecode and just select the Decompile button and you’ll get a new source tag similar to the one in Figure 8.5:
This is not code you can actually compile but it helps you get an idea of what’s happening. When you define the AppModule as a class, you get code like this:
public final class AppModule {
@Provides
@NotNull
public final SequenceGenerator provideSequenceGenerator() {
return (SequenceGenerator)(new NaturalSequenceGenerator(0));
}
}
When you use an object instead, AppModule’s code looks like this:
public final class AppModule {
public static final AppModule INSTANCE;
@Provides
@NotNull
public final SequenceGenerator provideSequenceGenerator() {
return (SequenceGenerator)(new NaturalSequenceGenerator(0));
}
private AppModule() {
}
static {
AppModule var0 = new AppModule();
INSTANCE = var0;
}
}
In this code, you can recognize the implementation of the Singleton pattern. This is the code Dagger processes to generate code. If you use a class, Dagger will create an instance of AppModule to delegate the creation of the SequenceGenerator<T> implementation. If you use an object, Dagger doesn’t create an instance, but uses the existing one instead.
You can easily compare how Dagger generates the code in these two cases by looking at the build folder.
In theory, you could make AppModule abstract. Try it out by changing the code of AppModule.kt, like this:
@Module(includes = [AppBindings::class])
abstract class AppModule { // HERE
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
Building the app now results in an error with the following message:
AppComponent.java:8: error: com.raywenderlich.android.raysequence.di.AppModule is abstract and has instance @Provides methods. Consider making the methods static or including a non-abstract subclass of the module instead.
Dagger is complaining that provideSequenceGenerator() is not static. That’s because Dagger needs an instance of AppModule, but it’s abstract — and you can’t create an instance of abstract classes. It’s also true that AppModule doesn’t have any state. provideSequenceGenerator() could be static, though. How can you do that in Kotlin? You’ll see in the next step.
Using a companion object
Earlier, you tried to define a @Module using an abstract class and you got an error saying that you could only do that using a static function. To fix the problem, you need a companion object. Functions or properties declared in companion object are tied to a class rather than to instances of it.
Open AppModule.kt and add the following:
@Module(includes = [AppBindings::class])
abstract class AppModule { // 1
// 2
companion object {
// 3
@Provides
// 4
@JvmStatic
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
}
As you can see, in that code you:
- Define the
AppModuleas an abstract class. - Use a
companion object. - Use
@Providesto annotateprovideSequenceGenerator(), which is now a function of the companion object. - Use the Kotlin
@JvmStaticannotation to tell the compiler that it should generate staticprovideSequenceGeneratorfunction in the enclosingAppModuleclass.
Build and run to confirm that Dagger’s happy now and everything works.
Using Dagger’s Lazy interface
In the previous paragraphs, you saw that @Module contains information about how to create an instance of an object in the dependency graph. Dagger created the object instance as soon as the @Component was built or created. However, this operation can impact the cold start time of the app.
The cold start time is how long the app takes to start from scratch. This includes the time used to load and launch the app, display a starting window, create the process for the app, launch the main thread, create the main Activity, inflate the views, lay out the screen and perform the initial draw. The cold start time should always be less than two seconds.
Note: The following example is just a way to show how the
Lazy<T>interface works in Dagger. The best way to improve the cold start time is to run the heavy code off the main thread, which is outside the scope of this book.
Suppose you want to simulate creating an object of the graph for the RaySequence app that is expensive in terms of taking a lot of time to load. Open NaturalSequenceGenerator.kt and add the init block with the following code:
class NaturalSequenceGenerator(private var start: Int) : SequenceGenerator<Int> {
init {
sleep(3000) // HERE
}
override fun next(): Int = start++
}
Here, you added an init() block with a three-second sleep to simulate the lag from creating an expensive object. Build and run and you’ll notice some delay, but it would be better to have an objective measure of how long it takes.
How can you measure the cold start time for the RaySequence app? Since Android 4.4, this information is easy to get. Just look at the LogCat window in the bottom part of Android Studio and filter the log using the text “Displayed”. Also, select the No Filter option in the combo on the right, as in Figure 8.6
You can’t build and run the app yet, however, because the Activity for the LAUNCH is SplashActivity, which doesn’t contain @Component’s initialization. To help with this, there’s already a build type named coldstart in the project.
coldstart uses a different AndroidManifest.xml, which configures MainActivity as the one to launch when you start the app.
Now, open the Build Variant Window on the left side of Android Studio and select coldstart for the app and mvp modules, as in Figure 8.7:
Now, you can finally build and run, then check the value you get in the LogCat window. You should see this output:
system_process I/ActivityTaskManager: Displayed com.raywenderlich.android.raysequence/.MainActivity: +3s884ms
In this case, the cold start time is 3 seconds and 884 ms. Of course, this is due to the sleep(3000) you introduced in the init block of NaturalSequenceGenerator.
So the model is slowing the app down at start time — but you don’t need that model until you press the button on the screen. This means you could create NaturalSequenceGenerator later, making the app start more quickly. Dagger allows you to use the Lazy<T> interface to delay the creation of an object.
Note: It’s important to distinguish Dagger’s
dagger.Lazy<T>interface from Kotlin’s similarly namedkotlin.Lazy<T>.
To see how it works, open SequencePresenterImpl.kt and apply the following:
@Singleton
class SequencePresenterImpl @Inject constructor() :
BasePresenter<MainActivity, SequenceViewBinder>(),
SequencePresenter {
@Inject
lateinit var sequenceModel: dagger.Lazy<SequenceGenerator<Int>> // 1
override fun displayNextValue() {
useViewBinder {
showNextValue(sequenceModel.get().next()) // 2
}
}
// ...
}
Here, you can see two very important things:
- The type for the dependency is now
dagger.Lazy<SequenceGenerator<Int>>. - Now that the
sequenceModelis of typeLazy<SequenceGenerator<Int>>, you need to invokeget()to get the reference to the specificSequenceGenerator<Int>>implementation.
Build and run and you’ll get a cold start time similar to the following:
system_process I/ActivityTaskManager: Displayed com.raywenderlich.android.raysequence/.MainActivity: +799ms
Now, the cold start time is 799ms, much smaller than the previous start time. Of course, you pay a price for this when you need the SequenceGenerator<Int> implementation. The first time you click the button, you’ll notice the delay when the model creation takes a while.
Finally, it’s important to note that:
- If you define a dependency type with
Lazy<T>, you get a faster startup time for free. You just need to provide the object of type T. Dagger applies the laziness automatically. - You create the object the first time you invoke
get()and, after that, you’ll get always the same instance. However,Lazy<T>is not like@Singleton. The former is an optimization, the latter is a matter of scope. You’ll learn more about this later.
Lazy<T> is a good tool, but it’s not the solution to every problem.
Before you move on, remember to restore the current build type to debug and to remove delay(3000) from NaturalSequenceGenerator.
Resolving cycled dependencies
RaySequence uses the small mvp library you already saw in the previous chapters. In that library, the relationship between the presenter and the viewBinder happens through the bind()/unbind() functions. This is how you pass the reference of the SequenceViewBinder implementation to the implementation of SequencePresenter into MainActivity, as you see in this code:
class MainActivity : AppCompatActivity() {
@Inject
lateinit var presenter: SequencePresenter
@Inject
lateinit var viewBinder: SequenceViewBinder
override fun onStart() {
super.onStart()
presenter.bind(viewBinder) // HERE
}
override fun onStop() {
presenter.unbind() // HERE
super.onStop()
}
// ...
}
This is fine, but what if you want to manage the dependency between the presenter and the viewBinder using Dagger? You’ll see how to do that next.
Adding a new implementation
Create a new file named CycledSequencePresenter.kt in RaySequence’s presenter and add the following code:
@Singleton
class CycledSequencePresenter @Inject constructor(
private val viewBinder: SequenceViewBinder // 1
) : SequencePresenter { // 2
@Inject
lateinit var sequenceModel: SequenceGenerator<Int>
override fun displayNextValue() {
viewBinder.showNextValue(sequenceModel.next())
}
// 3
override fun bind(viewBinder: SequenceViewBinder) {}
override fun unbind() {}
override fun onNextValuePressed() {
displayNextValue()
}
}
This is just another implementation of the SequencePresenter interface that:
- Receives the reference to the
SequenceViewBinderas a primary constructor parameter. - Doesn’t extend the
BasePresenterutility class. - Has empty implementation for the
bind()andunbind()operations.
Telling Dagger to use the new implementation
Now, you need to tell Dagger to use this implementation instead of SequencePresenterImpl. Open AppBindings.kt and replace @Binds in the SequencePresenter interface with this:
@Module
abstract class AppBindings {
// ...
@Binds
abstract fun bindSequencePresenter(impl: CycledSequencePresenter): SequencePresenter
}
Now you’re telling Dagger to use an instance of CycledSequencePresenter every time it needs an implementation of the SequencePresenter interface.
Encountering a cycled dependency error
You can now build the app — but something’s wrong. Dagger is complaining, giving you this error:
error: [Dagger/DependencyCycle] Found a dependency cycle:
Injecting the SequenceViewBinder in the SequencePresenter has created a cycled dependency. The name Dagger comes from Direct Acyclic Graph, which means it doesn’t like cycle dependencies. But what is the cycle?
You can read the stack trace in the error message or look at the UML diagram in Figure 8.8 to find out:
In this diagram, you can see that:
-
CycleSequencePresenterdepends onSequenceViewBinderImplthrough theSequenceViewBinder. -
SequenceViewBinderImpldepends onCycleSequencePresenterthrough theSequenceViewBinder.Listener.
You need to break this cycle. But how can you do that? Lazy<T> comes to the rescue.
Resolving the problem with Lazy
Open CycledSequencePresenter.kt and change it like this:
@Singleton
class CycledSequencePresenter @Inject constructor(
private val viewBinder: dagger.Lazy<SequenceViewBinder> // 1
) : SequencePresenter {
override fun displayNextValue() {
viewBinder.get().showNextValue(sequenceModel.next()) // 2
}
// ...
}
In this code, you:
- Change the type of the
viewBinderprimary constructor parameter toLazy<SequenceViewBinder>. - Use
get()to get the reference to the actualSequenceViewBinderimplementation.
Now, you’ll be able to successfully build the project. When you try to run, however, you get an error that you’ve already met before, in SequenceViewBinderImpl:
lateinit property output has not been initialized
That’s because, when you press the button on the screen, Dagger creates the first instance of the lazy SequenceViewBinder implementation — which is different from the one it already injected into MainActivity.
Now, you might think that using Lazy<SequenceViewBinder> everywhere would be a solution. Unfortunately, this isn’t true. Lazy is not a scope. The laziness of viewBinder in CycledSequencePresenter is local to that injection.
To verify this, open MainActivity.kt and change it to this:
class MainActivity : AppCompatActivity() {
@Inject
lateinit var presenter: SequencePresenter
@Inject
lateinit var viewBinder: dagger.Lazy<SequenceViewBinder>
override fun onCreate(savedInstanceState: Bundle?) {
DaggerAppComponent.create().inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewBinder.get().init(this)
}
}
In this case, the Lazy you define in the MainActivity differs from the one in CycledSequencePresenter. In short, Dagger will create two distinct instances: one for the MainActivity and one for the CycledSequencePresenter.
Before continuing, build and run to check that the app still crashes with the same error.
A possible solution to the previous problem is to annotate SequenceViewBinderImpl with @Singleton, like this:
@Singleton // HERE
class SequenceViewBinderImpl @Inject constructor(
private var sequenceViewListener: SequenceViewBinder.Listener,
private val context: Context
) : SequenceViewBinder {
// ...
}
Now you can build and run successfully — but there’s a but! You just used the Lazy type to solve a problem that had nothing to do with performance. You needed to break a cycle and not to defer the creation of an object. But there could be a better solution: Provider<T>.
Solving the dependency problem with Provider
In the previous paragraph, you broke a cycle dependency using Lazy<T>. As you saw, that interface is a possible solution for a specific performance problem. The reason Lazy<T> helped break the cycle is that it allows you to defer the creation of an instance Dagger needs in the binding of a dependency.
If you just need to defer something without the caching feature Lazy<T> provides, Provider<T> is the interface for you.
Open CycledSequencePresenter.kt and replace Lazy<T> with Provider<T>, like so:
@Singleton
class CycledSequencePresenter @Inject constructor(
private val viewBinder: Provider<SequenceViewBinder> // 1
) : SequencePresenter {
override fun displayNextValue() {
viewBinder.get().showNextValue(sequenceModel.next()) // 2
}
// ...
}
In this code, you:
- Replaced
dagger.Lazy<T>withjavax.inject.Provider<T>. - Didn’t change the way you get the reference to the provided instance. You still use
get().
It’s worth mentioning that Provider<T> isn’t a Dagger interface. Like @Inject, it’s part of Java Specification Request 330.
Now, when you build and run, everything works fine. But there’s something important to mention. Open CycledSequencePresenter.kt and change displayNextValue() like this:
@Singleton
class CycledSequencePresenter @Inject constructor(
private val viewBinder: Provider<SequenceViewBinder>
) : SequencePresenter {
override fun displayNextValue() {
val binder = viewBinder.get() // 1
Log.d("DAGGER_LOG", "Binder: $binder") // 2
binder.showNextValue(sequenceModel.next())
}
// ...
}
Here you:
- Invoke
get()on theProvider<SequenceViewBinder>every time you click the button. - Log out the reference of the binder.
Build and run the app, then click the Button and use *DAGGER_LOG as the filter. You’ll get a log like this:
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa
// ...
The object you get from Provider<T> is always the same — but this isn’t because of Provider<T> itself, but because of the @Singleton annotation you used on CycledSequencePresenter.
When you invoke get() on a Provider, Dagger resolves the object for the related type. Take a quick moment to prove this. Create a new file named RandomModule.kt in the di package and add the following code:
@Module
class RandomModule {
@Provides
fun provideRandomInt(): Int = Random.nextInt()
}
Now, add this module to the ones in AppComponent in AppComponent.kt, as in this code:
@Component(modules = [
AppModule::class,
RandomModule::class // HERE
])
@Singleton
interface AppComponent {
// ...
}
Finally, change CycledSequencePresenter to this:
@Singleton
class CycledSequencePresenter @Inject constructor(
private val viewBinder: Provider<SequenceViewBinder>,
private val randomProvider: Provider<Int> // HERE
) : SequencePresenter {
override fun displayNextValue() {
val binder = viewBinder.get()
Log.d("DAGGER_LOG", "Binder: $binder ${randomProvider.get()}") // HERE
binder.showNextValue(sequenceModel.next())
}
// ...
}
Here you:
- Inject a
Provider<Int>with therandomProviderprimary constructor parameter. - Invoke
get()on therandomProviderevery time you print a log message.
Now, build and run and click the Next button a few times. You’ll get an output similar to this:
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa 1771794424
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa -323421530
// ...
As you see, randomProvider provides a different value every time you invoke get().
As a good exercise, check what happens if you inject a Lazy<Int> instead. Moreover, check what happens if you inject two different Lazy<Int>s. Enjoy!
Key points
- A
@Modulecan include other modules by using theincludesattribute. - When you define a
@Modulewith an interface or an abstract class, it doesn’t change the code Dagger generates. - Dagger parses the Java equivalent of your Kotlin to generate its code.
- Don’t confuse the Kotlin
Lazy<T>with Dagger’sdagger.Lazy<T>. -
dagger.Lazy<T>lets you defer creating an object of the dependency graph. -
dagger.Lazy<T>is not a scope. - You can break cycle dependencies using the
Provider<T>interface.
In this chapter, you’ve learned more about Dagger @Modules. You saw how to structure different @Modules and how to use dagger.Lazy<T> and Provider<T>, depending on the problem you want to solve. In the next chapter, you’ll dig in even deeper and learn more about Dagger @Modules.