Best practices for implementing dagger in multi module projects

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

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

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?

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

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

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

Senior Android Engineer at Cognizant Softvision