Using Dagger in a multi-module project

Marcos Holgado
ProAndroidDev
Published in
8 min readDec 15, 2018

--

A bit more than a year ago I started to modularize our codebase. Back then, there was a lot of talking around the subject but no one really focused on how you could use Dagger in a multi-module environment which was one of the main problems I had. In this article I want to explain the approach we ended up taking a year ago and that we are still using.

Note: This article assumes you already know the basics of Dagger.

What we had

I started (like many others) with a monolithic codebase where everything was built within the same module, the main app. In our case, that was even more complicated because we were in fact building 4 different apps using the same codebase but that is a different story. Inside that monolithic app we had our features kind of separated using a feature-package approach (check that awesome article by Joe Birch). Dagger wise, our app had a MainComponent with shared dependencies. Each feature package had its own subcomponent which had MainComponent as parent to get all the dependencies.

In a simplified world the code would look something like this:

By doing this, we have access to our ExpensiveObject from all the subcomponents that have MainComponent as a parent. In other words, our features have a dependency on the MainComponent, although in reality this is not 100% true. Yes, the subcomponents have a parent but it is the parent component the one that refers to the children in code. In the snippet shown above, you can see that MainComponent needs to know about MySubcomponent, that dependency hierarchy would become one my worst nightmares.

    fun plus(mySubModule: MySubModule): MySubcomponent

What we really have is a circular dependency. MainComponent depends on our subcomponents but our features also have a dependency on ExpensiveObject which is part of the main package in our app.

Let’s talk about how we can actually implement this. We could start by doing something like the code shown below.

This would work just fine but, remember, every time you build MainComponent you are in fact creating all the dependencies again, so even though we marked our ExpensiveObject as a @Singleton that won’t matter and we will have a different instance in each activity.

A simple an easy way to fix that is by moving the creation of the MainComponent to your Application.

So now we can use our new provideMainComponent() method to get the instance of the component and use it wherever we need it. The casting makes it a little bit ugly but you can always create a helper method or an extension function, I’ll leave that out of this article though.

Using the new method in our MainActivity it would look like this:

(application as MyApplication).provideMainComponent().inject(this)

And using it in our OtherActivity it would look like this:

(application as MyApplication).provideMainComponent()
.plus(MySubModule())
.inject(this)

Modularizing (aka playing Jenga)

So far we have seen a really simple example of what we had. We now have to start thinking about how can we take one of those feature packages and move it to its own module. In theory it looks like a simple exercise but in reality is like playing Jenga, one tiny mistake moving dependencies will make the whole project fall apart pretty quickly.

Let’s see what happens if we move our feature to a module.

Right off the bat we can see this is not going to work. The fact that we have a circular dependency should tell us that there is something wrong with this.

Our app via the MainComponent needs to know about our feature, which makes sense because if we want to integrate that feature we are going to have to depend on it. We modify our gradle.build file from our app module a little bit and we fix that issue.

implementation project(path: ':feature1')

That dependency has been now fulfilled. However, our feature needs to know about our ExpensiveObject. We can’t create that dependency in our new module (you can but… yeah… don’t) so we need to figure out a different way of doing this.

The most obvious thing we can do is to extract that dependency into another module. Let’s think about that for a second. Is that a dependency you are going to share with other modules? or can you move it into the new feature module? In our case, let’s say that we want to share that dependency and we actually want to share the same instance of that dependency.

After extracting ExpensiveObject to another module (I’ve called it core) this is how the whole project looks like.

It is starting to make more sense, however, we are not using Dagger in our core module yet. The problem that we now have is that, if we keep using a subcomponent in our feature module, we are then going to have another circular dependency with core because core will need to know about the subcomponent.

At this point it just seems a good idea to ditch subcomponents, at least when we are trying to connect different modules.

It is a pretty obvious decision but it took me a while to realize that there was no way to use Dagger in a simple way unless we got rid of subcomponents. At this stage I had already tried to override modules to pass dependencies from the app, implement crazy interfaces, store components in even crazier ways… Trust me, is not worth it.

Keep It Simple Stupid!

Let’s remove all the subcomponents then and create a new component in core (CoreComponent). Our new feature components will now depend on CoreComponent.

If we implement these changes our CoreComponent and CoreModule would look like this:

@Component(modules = [CoreModule::class])
@Singleton
interface CoreComponent {
fun getExpensiveObject(): ExpensiveObject
}

@Module
class CoreModule {
@Provides
@Singleton
fun provideExpObj(): ExpensiveObject = ExpensiveObject()
}

Remember that we wanted to share the same instance of our ExpensiveObject hence why using @Singleton, also, notice how we have added a new getExpensiveObject() method. The reason behind this is because when we are building component dependencies we need to surface them in the parent component in order for them to be used by the child components.

Our Feature1Component and Feature1Module would look like this:

@Component(modules = [Feature1Module::class], 
dependencies = [CoreComponent::class])
interface Feature1Component {
fun inject(activity: OtherActivity)
}
@Module
class Feature1Module {
@Provides
fun provideString() = "test"
}

And similarly our Feature2Component and Feature2Module:

@Component(modules = [Feature2Module::class], 
dependencies = [CoreComponent::class])
interface Feature2Component {
fun inject(mainActivity: MainActivity)
}
@Module
class Feature2Module {
@Provides
fun provideInt() = 1
}

And to use the new components you will have to do something like this in your activities/fragments:

val coreComponent = DaggerCoreComponent.builder().build()

DaggerFeature1Component
.builder()
.coreComponent(coreComponent)
.build()
.inject(this)

But we are again creating the CoreComponent in every activity so we are back to one of our main problems, we need a way to share that CoreComponent instance across our modules. But I’ll come back to this later, before we need to talk about scopes.

Scopes

When dealing with component dependencies we must follow 2 simple rules:

  1. An un-scoped component cannot depend on scoped components.
  2. A scoped component cannot depend on a component with the same scope.

None of our feature components are scoped but they depend on a scoped component so we are breaking the first rule. Also, we cannot use the @Singleton scope because we would be breaking the second rule. So, what do we do? We just create a new scope and use it in our feature components.

I’m going to name this scope as @FeatureScope and will put it into our core module since I want to be able to reuse it in our different feature modules.

@Scope
@Retention
annotation class FeatureScope

We can finally use it in our feature components to stop breaking the rules we talked about above.

@Component(modules = [Feature1Module::class], 
dependencies = [CoreComponent::class])
@FeatureScope
interface Feature1Component {
fun inject(activity: OtherActivity)
}
@Component(modules = [Feature2Module::class],
dependencies = [CoreComponent::class])
@FeatureScope
interface Feature2Component {
fun inject(mainActivity: MainActivity)
}

Sharing CoreComponent across modules

One of the most important things we wanted was to share the same instance of our ExpensiveObject across our different modules. That’s why we used the @Singleton annotation, however, so far we are still creating a new CoreComponent in every feature module, which means, we are creating a new instance of ExpensiveObject every time.

The plan to avoid this is to create an interface that our application will have to implement, since the application is just an application context we can then take that context from our activities/fragments in our feature modules, check if it implements this interface and then get the core component from it. To avoid writing the same checks everywhere we will have a helper class as well.

Putting all together we end up with this:

The actual implementation of the interface is exactly the same as we saw at the beginning of the article. We can then just use it in our feature modules since they will have access to the application context and we will finally be sharing the same instance of CoreComponent across our modules.

That was everything, you should now be able to use Dagger in a multi-module project and sharing those dependencies in a very simple an easy way.

If you want to check out the code just head to: https://github.com/marcosholgado/dagger-playground/tree/multi-module

If you want to see this strategy in action, you can also check the sample app I wrote for my Droidcon UK talk here: https://github.com/marcosholgado/droidcon18

If you want to know how to use dagger-android instead, please head up to this other article.

Finally, if you have any questions please reach out on Twitter or just leave a comment.

--

--

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