Best practices for implementing dagger in multi module projects

Federico Torres
4 min readOct 22, 2019

When you have a big application with more than ten or twenty modules, dagger will give you some headaches if it is not implemented right.

The problem

Imagine an app with a lot of modules, in this app to inject some object to module A you need to add a dependency to another module B, but what happens if module A already depends on B? You have a circular dependency, and also you should know which module provides that object, believe me in large projects it is not good.

But worst, this nightmare of dependencies between modules provokes that you waste the best advantages of a multi-module project, parallelize building is worthless, and build time is increased.

My solution

I came across with some good solutions to multi-module projects but none was facing my issue

Every feature module should be able to get any injectable object without needing to depend on a particular module

So for this example I created four modules: app, core, login and utilities

Maybe after seeing this picture you are a bit confused, but don’t panic in the next lines I will implement all this modules and dependencies.

First we will create the project and add all the modules mentioned above.

After that we must add dependencies to the build.gradle files for each module

App Module//Dagger
api "com.google.dagger:dagger:$rootProject.dagger"
kapt "com.google.dagger:dagger-compiler:$rootProject.dagger"
api "com.google.dagger:dagger-android:$rootProject.dagger"
kapt "com.google.dagger:dagger-android-processor:$rootProject.dagger"
//Modules
implementation project(path: ':core')
implementation project(path: ':login')
implementation project(path: ':utilities')
Core Module//Dagger
api "com.google.dagger:dagger:$rootProject.dagger"
kapt "com.google.dagger:dagger-compiler:$rootProject.dagger"
api "com.google.dagger:dagger-android:$rootProject.dagger"
kapt "com.google.dagger:dagger-android-processor:$rootProject.dagger"
Login module//Dagger
api "com.google.dagger:dagger:$rootProject.dagger"
kapt "com.google.dagger:dagger-compiler:$rootProject.dagger"
api "com.google.dagger:dagger-android:$rootProject.dagger"
kapt "com.google.dagger:dagger-android-processor:$rootProject.dagger"
//Modules
implementation project(path: ':core')
Utilities Module//Dagger
api "com.google.dagger:dagger:$rootProject.dagger"
kapt "com.google.dagger:dagger-compiler:$rootProject.dagger"
api "com.google.dagger:dagger-android:$rootProject.dagger"
kapt "com.google.dagger:dagger-android-processor:$rootProject.dagger"
//Modules
implementation project(path: ':core')

So after this initial module configuration, we can start coding our dagger related files.

The first one will be the AppComponent

@Component(
modules = [
AndroidInjectionModule::class,
ActivityBindingModule::class
],
dependencies = [CoreComponent::class, UtilsComponent::class]
)
@AppScope
interface AppComponent : AndroidInjector<MainApplication> {

@Component.Builder
interface Builder {
@BindsInstance
fun application(application: Application): AppComponent.Builder

fun coreComponent(coreComponent: CoreComponent): AppComponent.Builder
fun utilsComponent(utilsComponent: UtilsComponent): AppComponent.Builder
fun build(): AppComponent
}
}

Here we are creating the AppComponent adding its module and component dependencies, It ‘s worth mentioning the dependencies between components, this will allow us to provide all the objects provided by Core and Utils components to all the subcomponents of AppComponent, this is the key to what we are looking for.

ActivityBindingModule is where we will provide the injection for our activities

@Module
abstract class ActivityBindingModule {

@ContributesAndroidInjector
abstract fun mainActivity(): MainActivity

@ContributesAndroidInjector(modules = [LoginModule::class])
abstract fun loginActivity(): LoginActivity
}

The key to understand why this works, is that each time we add a @ContributesAndroidInjector annotation, we are creating a subcomponent of AppComponent so each of this subcomponents will be able to inject any object of Core, App and Utils components (think it as inheritance)

@Module
class LoginModule {
@Provides
fun provideFoo() = Foo()
}
@Component
@Singleton
interface CoreComponent {
fun getUserController(): UserController

}

Here is something important to say: An scoped component cannot depend on a component with the same scope, as AppComponent has @AppScope here we define CoreComponent with @Singleton scope, also you could create a new scope called @CoreScope.

Here UserController will have a single instance for all the application

@Component(modules = [UtilsModule::class])
interface UtilsComponent {

fun getResourceProvider() : IResourceProvider

@Component.Builder
interface Builder {
@BindsInstance
fun application(application: Application): UtilsComponent.Builder

fun build(): UtilsComponent
}
}
@Module
abstract class UtilsModule {

@Binds
abstract fun bindContext(application: Application): Context

@Module
companion object {
@Provides
@JvmStatic
fun bindResourceProvider(context: Context): IResourceProvider {
return ResourceProviderImpl(context)
}
}

}

Here we are providing an interface for inflating android related resources, this class needs a context that is why we created a method to pass the application object and later we bind it to context.

So we need to do one more thing to make this work, we need to create a custom Application class to create the AppComponent

class MainApplication : DaggerApplication() {


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

}


fun provideCoreComponent(): CoreComponent {
return DaggerCoreComponent
.builder()
.build()
}

private fun getUtilsComponent(): UtilsComponent {
return DaggerUtilsComponent
.builder()
.application(this)
.build()
}


}

Now we can add all the modules we want on the project with just one module dependency: Core

Why Core Module?

Because this module must contain all the interfaces that will be injected in feature modules, IResourceProvider is at core module, but ResourceProviderImpl is on utilities module, so core module does not contain the logic to provide resources, just the interface.

class ResourceProviderImpl(val context: Context) : IResourceProvider {

override fun getString(resourceId: Int): String {
return context.getString(resourceId).orEmpty()
}

override fun getString(id: Int, vararg args: Any?): String {
return context.getString(id, *args)
}


override fun getDrawable(drawableRes: Int): Drawable {
return requireNotNull(ContextCompat.getDrawable(context, drawableRes))
}
}

Injecting dependencies

Now we can do something like this:

class LoginActivity : AppCompatActivity() {

@Inject
lateinit var foo: Foo()

@Inject
lateinit var userController: UserController

@Inject
lateinit var resourceProvider: IResourceProvider

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


val textView = findViewById<TextView>(R.id.textView)
textView.text = resourceProvider.getString(R.string.app_name)
}
}

Note this: login module do not depend on utilities module, but the injection is successful

Conclusion

I hope this helps you simplify your dagger implementation on large multi module projects, this helps improving build time and keeping a clean app architecture.

Update: I made a sample app showing how I implemented this: https://github.com/fededri/DaggerMultiModulesDemo

--

--