Using Dagger-Android in a multi-module project

Marcos Holgado
ProAndroidDev
Published in
11 min readDec 21, 2018

--

In my last article I talked about how to use Dagger in a multi-module project, if you haven’t read it yet, I would recommend you do it so before continuing since we will be reusing the same project. After I published the article I got a lot of questions about how I would approach a multi-module project using dagger-android instead. In this article I will talk about dagger-android and re-implement what we did in the last article but this time using dagger-android instead.

Photo by Simon Goetz on Unsplash

Before I begin, remember that you can find the code of the previous article in the following GitHub repository (multi-module branch):

https://github.com/marcosholgado/dagger-playground/tree/multi-module

The code from this article will be available in two different branches. I will start by migrating to dagger-android following the documentation without taking any shortcuts. The idea is that hopefully you will understand better what’s going on. You can find the code here:

https://github.com/marcosholgado/dagger-playground/tree/multi-module-dagger-android-part1

After that I will refactor that code to remove more boilerplate by taking a few shortcuts. You can find that code here:

https://github.com/marcosholgado/dagger-playground/tree/multi-module-dagger-android

Why dagger-android?

The idea behind dagger-android is to reduce the boilerplate needed to inject objects. To be even more specific, the idea is to reduce that boilerplate code in Fragments, Activities or any other Android framework classes that are instantiated by the OS. Since we don’t have any control over those type of classes we will have to default to member injection which introduces boilerplate.

But there is another problem, dagger also requires the class needing the injected objects to know about the provider of those objects. In our MainActivity that we used in our previous article, the activity had to know about Feature2Component and also about CoreComponent as you can see below.

class MainActivity : AppCompatActivity() {

@Inject
lateinit var expensiveObject: ExpensiveObject

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

DaggerFeature2Component
.builder()
.coreComponent(CoreInjectHelper
.provideCoreComponent(applicationContext))
.build()
.inject(this)
}
}

This code has a few problems that are really well-explained on the dagger-android documentation.

1. Copy-pasting code makes it hard to refactor later on. As more and more developers copy-paste that block, fewer will know what it actually does.

2. More fundamentally, it requires the type requesting injection (Activity) to know about its injector. Even if this is done through interfaces instead of concrete types, it breaks a core principle of dependency injection: a class shouldn’t know anything about how it is injected.

The subcomponent conundrum

If you remember from the previous article, one of the problems we had was having circular dependencies when using subcomponents. Your component needs to know about the subcomponent which also needs to know about the component. If that component lives in a separate module (in our case Core) you then end up with a circular dependency.

F1SubComponent depends on CoreComponent, but at the same time, CoreComponent needs to know about the F1SubComponent. Because of this circular dependency we previously ditched subcomponents in favour of component dependency which worked out really well.

We ended up with the following architecture.

Dagger-android and going back to subcomponents

But Marcos, you’ve just said that subcomponents are not ideal in a multi-module project, why are we going back to use them? Because dagger-android, in a nutshell, is a collection of classes that are going to help us tackling the issues I mentioned before and it turns out they use subcomponents.

Let’s begin with the implementation. To make it easier I will simply follow the dagger-android documentation step by step and we’ll see where that takes us. After that we’ll have another look at the code to see if we can refactor it a little bit more to remove more boilerplate.

Migrating to dagger-android

I will be honest, migrating to dagger-android is really simple but understanding what’s going on behind the scenes is a different matter. To me, that is the main issue, yes you can use dagger-android but at some point you will need to know exactly what it does plus, let’s be honest, we usually want to understand how things work and not just rely in “magic”.

Without getting into much detail, the general idea is to perform member injection of the Android framework classes: Activities, Fragments, BroadcastReceivers, Services and ContentProviders. That’s done by injecting a map from each concrete class into an AndroidInjector.Factory for an AndroidInjector of that class.

“In english please?”

We have to create different injectors for our activities, fragments, etc, that will perform the injection of the objects. We then put them in a map so whenever we need to inject our objects into an activity/fragment we can retrieve the injector of that activity/fragment from that map to perform the injection. Luckily for us, dagger-android gives us a lot of classes that are going to help us during the process.

Before we begin let’s have a look at our project. Compared to the previous article I’ve renamed Feature2Component to AppComponent because we are back to using subcomponents and we need a parent for all of them. That parent has to live inside our application because it’s the only common point for all our modules.

Step 1

The first step is installing AndroidInjectionModule in our application component to ensure that all the necessary bindings for our base types are available. We also remove the old inject(mainActivity: MainActivity) method because we won’t need it anymore, instead we are going to have to inject objects in MyApplication.

Also, to make scopes a little more clear I have created a new @AppScope in the application which will be used by our AppComponent. In the previous article I mentioned two rules that we have to follow, hence why we still need a different scope than the we one we are using in Core (@Singleton).

@Component(modules = [AndroidInjectionModule::class,
AppModule::class],
dependencies = [CoreComponent::class])
@AppScope
interface AppComponent {
fun inject(application: MyApplication)
}

Step 2

In our application we have to implement HasActivityInjector because we are only using Activities in this example. If you want to use Fragments, the approach is pretty similar though.

class MyApplication : Application(), 
CoreComponentProvider,
HasActivityInjector {

@Inject
lateinit var activityInjector:
DispatchingAndroidInjector<Activity>


private lateinit var coreComponent: CoreComponent
override fun onCreate() {
super.onCreate()
DaggerAppComponent
.builder()
.coreComponent(provideCoreComponent())
.build()
.inject(this)
}
override fun activityInjector(): AndroidInjector<Activity> =
activityInjector


override fun provideCoreComponent(): CoreComponent {
if (!this::coreComponent.isInitialized) {
coreComponent = DaggerCoreComponent
.builder()
.build()
}
return coreComponent
}
}

Here is where we start to take advantage of dagger-android, rather than having to write our own activity injector we can use DispatchingAndroidInjector<Activity> which already implements all the logic needed to find our activity injectors.

Let’s also think about the provideCoreComponent method for a second. We know we are still going to need the dependencies from Core so our AppComponent will still have to depend on it. I cannot use a subcomponent in core because we would have a circular dependency and dagger-android has no use in Core since we are not really injecting anything there, just providing dependencies.

Step 3

We now have to define a new subcomponent which implements AndroidInjector<YourActivityHere> with a @Subcomponent.Builder extending AndroidInjector.Builder<YourActivityHere>.

@Subcomponent
interface MainActivitySubcomponent: AndroidInjector<MainActivity> {
@Subcomponent.Builder
abstract class Builder: AndroidInjector.Builder<MainActivity>()
}

Then we have to create a new module that binds the subcomponent builder. Here is the part where we are adding our activity injector into the injectors map. Because we are adding something into a map we also need a key so we can retrieve the value afterwards, that key is the class where we will perform the injection and the value, like I said before, is the AndroidInjector.Factory.

@Module(subcomponents = [MainActivitySubcomponent::class])
abstract class MainActivityModule {
@Binds
@IntoMap
@ClassKey(MainActivity::class)
internal abstract fun bindMainActivityInjectorFactory(
builder: MainActivitySubcomponent.Builder
): AndroidInjector.Factory<*>
}

And we can finally add MainActivityModule to the modules list of our AppComponent.

@Component(modules = [AndroidInjectionModule::class,
AppModule::class,
MainActivityModule::class],
dependencies = [CoreComponent::class])
@AppScope
interface AppComponent {
fun inject(application: MyApplication)
}

Step 4

Back to our MainActivity we can now remove all the boilerplate and simply have the following code.

class MainActivity : AppCompatActivity() {
@Inject
lateinit var expensiveObject: ExpensiveObject

@Inject
lateinit var otherObject: OtherObject

override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
startActivity(Intent(this, OtherActivity::class.java))
}
}
}

At this point you can run the application and everything should work, including using the same instance of ExpensiveObject in MainActivity and OtherActivity. Notice that we haven’t touched our feature1 module yet but everything still works. The reason is because that module only depends on Core and we haven’t changed anything there yet. We still have some boilerplate code there so let’s get rid of it.

Step 5

We are going to follow the same approach in our feature module. We don’t a need a component anymore so we can go ahead and delete Feature1Component.

Same as before we have to create a new subcomponent. This subcomponent will use our old Feature1Module to provide the specific dependencies needed from this feature module.

@Subcomponent(modules = [Feature1Module::class])
interface OtherActivitySubcomponent: AndroidInjector<OtherActivity> {
@Subcomponent.Builder
abstract class Builder: AndroidInjector.Builder<OtherActivity>()
}

And again like before we need a new module to add the AndroidInjector.Factory into the injectors map.

@Module(subcomponents = [OtherActivitySubcomponent::class])
abstract class OtherActivityModule {
@Binds
@IntoMap
@ClassKey(OtherActivity::class)
internal abstract fun bindMainActivityInjectorFactory(
builder: OtherActivitySubcomponent.Builder
): AndroidInjector.Factory<*>
}

Finally, after removing the boilerplate from OtherActivity the end result looks like this:

class OtherActivity : AppCompatActivity() {

@Inject
lateinit var expensiveObject: ExpensiveObject

@Inject
lateinit var text: String

override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_other)
}
}

Is this even worth it?

So far we have followed the dagger-android documentation to get to this point. Our main goal was to remove boilerplate from our activities but have we actually done it?

https://github.com/marcosholgado/dagger-playground/commit/aecc4a71eec8695509ea81a892a8835b5511ecc9

That’s the commit where I migrated from dagger to dagger-android, funnily enough we have in fact introduced more code, we now need an extra 81 lines of code to do the exact same thing. The reason is because we need extra subcomponents and extra modules that we didn’t need before. We have deleted some boilerplate code in the activities but introduced a bit more in other places.

I would like more deletions :(

Refactoring with dagger-android

Can we remove even more boilerplate? It turns out that we can. Let’s begin with MyApplication.

We can extend from DaggerApplication and then implement applicationInjector() rather than having to implement activityInjector() and inject DispatchAndroidInjector. Overall we save a few lines of code, nothing crazy though.

class MyApplication : DaggerApplication(), CoreComponentProvider {

private lateinit var coreComponent: CoreComponent

override fun applicationInjector():
AndroidInjector<out DaggerApplication> {
return DaggerAppComponent
.builder()
.application(this)
.coreComponent(provideCoreComponent())
.build()
}


override fun provideCoreComponent(): CoreComponent {
if (!this::coreComponent.isInitialized) {
coreComponent = DaggerCoreComponent
.builder()
.build()
}
return coreComponent
}
}

By doing this we have to change our AppComponent as well to extend from AndroidInjector<YourApplicationHere>. Since our component depends on CoreComponent we will have to create a coreComponent method to provide that dependency when we create MainComponent from MyApplication. Here we actually have more lines of code than previously.

@Component(modules = [AppModule::class,
ActivityBindingModule::class,
AndroidSupportInjectionModule::class],
dependencies = [CoreComponent::class]
)
@AppScope
interface AppComponent : AndroidInjector<MyApplication> {
@Component.Builder
interface Builder {
@BindsInstance
fun application(application: Application):
AppComponent.Builder
fun coreComponent(coreComponent: CoreComponent):
AppComponent.Builder
fun build(): AppComponent
}

}

You may have notice that we are now using a new ActivityBindingModule that we didn’t have before. If your subcomponent and its builder have no other methods or supertypes than the ones we’ve used before we can use @ContributesAndroidInjector to let dagger-android generate that code automatically for us. This new module saves us quite a few lines of code since we are not going to create any subcomponents or modules to do the bindings that we had to do before.

@Module
abstract class ActivityBindingModule {
@ContributesAndroidInjector
abstract fun mainActivity(): MainActivity

@ContributesAndroidInjector(modules = [Feature1Module::class])
abstract fun otherActivity(): OtherActivity
}

This dagger module must live in your application and it’s going to be the glue that binds all your feature modules in a single place. That single place has to be your application because it is the only place in your code that’s going to have a dependency to the rest of the modules.

Our feature1 module still depends on Core because it needs a singleton instance of ExpensiveObject). However we are not specifying that dependency from Core in our feature1 module since we don’t need a subcomponent there anymore, so where is that dependency coming from?

It is in this ActivityBindingModule where we create the feature1 subcomponent that have AppComponent as parent which depends on CoreComponent satisfying all the dependencies. The dependency is still there but hidden behind the chain of dependencies.

Finally we can change our activities to extend from DaggerAppCompatActivity letting us remove AndroidInjection.inject(this).

class MainActivity : DaggerAppCompatActivity() {

@Inject
lateinit var expensiveObject: ExpensiveObject

@Inject
lateinit var otherObject: OtherObject

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
button.setOnClickListener {
startActivity(Intent(this, OtherActivity::class.java))
}
}
}

This is the final commit including all these changes

https://github.com/marcosholgado/dagger-playground/commit/7cd13ac8e403551771282b0aa7f554d885e23bd5

As you can see this is way better than what we had before, the extra lines are due to some minor boilerplate code and the new @AppScope that we’ve created. However there will be cases where you won’t be able to use these shortcuts, it will depend on how you architecture your application and your modules.

Final thoughts

Using dagger-android allows us to use subcomponents in a way that we can avoid having circular dependencies, it stills needs a bit of boilerplate but most importantly doesn’t break the main principle of dependency injection, the question is, is it worth it?

We have solved the circular dependency issue and now our injected classes don’t know about how its dependencies are being inject but, we have also introduced more complexity by using concepts like @Binds, @IntoMap, injector factories, etc. Am I against it? Absolutely not. My point is that if you want to use dagger or dagger-android you should know what it does behind the scenes, learn what the trade-offs are and then make an informed decision based on that.

Remember that you have the code of this article in these two branches of my dagger-playground repository.

https://github.com/marcosholgado/dagger-playground/tree/multi-module-dagger-android-part1

https://github.com/marcosholgado/dagger-playground/tree/multi-module-dagger-android

I hope you enjoyed this article and learned a bit more about dagger and dagger-android. If you still have some questions please do reach out on Twitter or leave a comment.

--

--

Senior Android Developer at DuckDuckGo. Speaker, Kotlin lover and I also fly planes. www.marcosholgado.com