8
Working With Modules
Written by Massimo Carli
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In previous chapters, you’ve used @Module
s 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 @Module
s for more than that. As the name implies, they’re also a way to group definitions. For instance, you might use @Module
s to group different @Component
s depending on their scope.
Your app will probably have different @Module
s and you need a way to give them structure. @Module
s can have dependencies as well.
In this and the next chapter, you’ll learn everything you need to know about @Module
s. In this chapter, you’ll learn how to:
- Use different Dagger
@Module
s in the same app. -
Optimize start-up performances using Dagger’s
Lazy<T>
interface. - Avoid cycled dependencies using the
Provider
interface. - Use optional bindings.
As you see, there’s a lot to learn about @Module
s.
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
@Module
with 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
}
@Module(includes = [AppBindings::class]) // HERE
object AppModule {
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
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
}
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.
@Module(includes = [AppBindings::class])
class AppModule { // HERE
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
public final class AppModule {
@Provides
@NotNull
public final SequenceGenerator provideSequenceGenerator() {
return (SequenceGenerator)(new NaturalSequenceGenerator(0));
}
}
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;
}
}
@Module(includes = [AppBindings::class])
abstract class AppModule { // HERE
@Provides
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
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.
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.
@Module(includes = [AppBindings::class])
abstract class AppModule { // 1
// 2
companion object {
// 3
@Provides
// 4
@JvmStatic
fun provideSequenceGenerator(): SequenceGenerator<Int> =
NaturalSequenceGenerator(0)
}
}
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.
class NaturalSequenceGenerator(private var start: Int) : SequenceGenerator<Int> {
init {
sleep(3000) // HERE
}
override fun next(): Int = start++
}
system_process I/ActivityTaskManager: Displayed com.raywenderlich.android.raysequence/.MainActivity: +3s884ms
@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
}
}
// ...
}
system_process I/ActivityTaskManager: Displayed com.raywenderlich.android.raysequence/.MainActivity: +799ms
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()
}
// ...
}
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()
}
}
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
}
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:
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
}
// ...
}
lateinit property output has not been initialized
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)
}
}
@Singleton // HERE
class SequenceViewBinderImpl @Inject constructor(
private var sequenceViewListener: SequenceViewBinder.Listener,
private val context: Context
) : SequenceViewBinder {
// ...
}
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.
@Singleton
class CycledSequencePresenter @Inject constructor(
private val viewBinder: Provider<SequenceViewBinder> // 1
) : SequencePresenter {
override fun displayNextValue() {
viewBinder.get().showNextValue(sequenceModel.next()) // 2
}
// ...
}
@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())
}
// ...
}
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa
// ...
@Module
class RandomModule {
@Provides
fun provideRandomInt(): Int = Random.nextInt()
}
@Component(modules = [
AppModule::class,
RandomModule::class // HERE
])
@Singleton
interface AppComponent {
// ...
}
@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())
}
// ...
}
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa 1771794424
D/DAGGER_LOG: Binder: com...SequenceViewBinderImpl@f4f3efa -323421530
// ...
Key points
- A
@Module
can include other modules by using theincludes
attribute. - When you define a
@Module
with 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.