Back to Stories

Youky's avatar

The Journal on Dagger 2 in a Multi-Module Android Project

  • Software Architecture

  • Android

This guide outlines how to manage shared dependencies between your :app module and feature modules (including dynamic feature modules) using Dagger 2, by introducing a :core module.

The Journal on Dagger 2 in a Multi-Module Android Projectthumbnail

This journal is a guide which outlines how to manage shared dependencies between your :app module and feature modules (including dynamic feature modules) using Dagger 2, by introducing a :core module.

Goal: Allow feature modules to access shared dependencies (like OkHttpClient, SharedPreferences, etc.) provided by the main application graph without directly depending on the :app module.

Modules Involved:

  • :app: Your main application module. Contains the Application class and the root Dagger component (AppComponent).
  • :core: A new library module. Will contain interfaces for shared dependencies and the provider interface. Both :app and feature modules will depend on :core.
  • :feature_example: A representative feature module (could be a regular library module or a dynamic feature module).

Step 1: Create the :core Module

  1. If it doesn’t exist, create a new Android Library module named core.
    • In Android Studio: File > New > New Module… > Android Library.
  2. Ensure Dagger dependencies are added to :core/build.gradle if you plan to use Dagger annotations within :core itself. For this pattern, it mainly holds interfaces, so Dagger dependencies might not be strictly needed in :core unless interfaces use Dagger annotations like @Scope.
// :core/build.gradle

plugins {
    id 'kotlin-ksp' // If using @Scope or other Dagger annotations in :core // }
dependencies {
    implementation "com.google.dagger:dagger:2.x" // If using @Scope //
    ksp "com.google.dagger:dagger-compiler:2.x" // If using @Scope //
    }

Step 2: Define Shared Dependency Interfaces in :core

  1. In the :core module, create an interface that lists the dependencies your features will need from the application graph.
// :core/src/main/java/com/yourcompany/core/di/CoreDependencies.kt package com.yourcompany.core.di

import android.content.Context
import retrofit2.Retrofit // Example dependency
// import com.yourcompany.core.analytics.AnalyticsService // Another example

interface CoreDependencies {
    fun applicationContext(): Context
    fun retrofit(): Retrofit
    // fun analyticsService(): AnalyticsService
    // Add other shared dependencies your features will need
}
  1. In the :core module, create an interface that your Application class will implement to provide these CoreDependencies.
// :core/src/main/java/com/yourcompany/core/di/CoreDependenciesProvider.kt package com.yourcompany.core.di

interface CoreDependenciesProvider {
    fun provideCoreDependencies(): CoreDependencies
}

Step 3: Configure the :app Module

  1. Add Dependency on :core: In :app/build.gradle:
// :app/build.gradle

plugins { id 'kotlin-ksp' }

dependencies {
    implementation project(":core")

    // ... other app dependencies
    implementation "com.google.dagger:dagger:2.x"

    // Use your Dagger version
    ksp("com.google.dagger:dagger-compiler:2.x")
}
  1. Define AppComponent in :app: This is your root Dagger component. It should implement the CoreDependencies interface from the :core module.
// :app/src/main/java/com/yourcompany/app/di/AppComponent.kt package com.yourcompany.app.diimport android.content.Context

import com.yourcompany.app.RecipeApplication
import com.yourcompany.core.di.CoreDependencies // Import from :core
import dagger.BindsInstance
import dagger.Component
import javax.inject.Singleton

@Singleton
@Component(modules = [AppModule::class, NetworkModule::class /* etc. */])
interface AppComponent : CoreDependencies { // Implement CoreDependencies

    fun inject(application: RecipeApplication) // For Application class injection

    // Factory to create the component
    @Component.Factory
    interface Factory {
        fun create(@BindsInstance applicationContext: Context): AppComponent
    }

    // You might still have provisions specific to the app module here
    // or expose them via CoreDependencies if features need them.
}
  1. Define Dagger Modules in :app (e.g., AppModule, NetworkModule): These modules will provide the concrete implementations for the dependencies listed in CoreDependencies.
// :app/src/main/java/com/yourcompany/app/di/NetworkModule.kt package com.yourcompany.app.di// import com.yourcompany.core.analytics.AnalyticsService // Assuming this is defined in core or app
import dagger.Module
import dagger.Provides
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import javax.inject.Singleton

@Module
object NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    /*
    @Singleton
    @Provides
    fun provideAnalyticsService(): AnalyticsService {
        return AnalyticsService() // Replace with actual implementation
    }
    */
}

Ensure AppModule (not shown here, but typically provides Context) and other modules provide all dependencies required by CoreDependencies.

  1. Modify Your Application Class in :app: Implement CoreDependenciesProvider and initialize AppComponent.
// :app/src/main/java/com/yourcompany/app/RecipeApplication.kt package com.yourcompany.appimport android.app.Application

import android.content.Context // For AppModule usually
import com.yourcompany.app.di.AppComponent
import com.yourcompany.app.di.DaggerAppComponent
import com.yourcompany.core.di.CoreDependencies
import com.yourcompany.core.di.CoreDependenciesProvider // Import from :core

class RecipeApplication : Application(), CoreDependenciesProvider {

    lateinit var appComponent: AppComponent
        private set // Make it accessible but not modifiable from outside

    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerAppComponent.factory().create(applicationContext)
        appComponent.inject(this) // If you need to inject Application itself
    }

    override fun provideCoreDependencies(): CoreDependencies {
        return appComponent
    }
}

You would also need an AppModule, typically providing the applicationContext if not done via @BindsInstance in the component factory directly for all uses.

// :app/src/main/java/com/yourcompany/app/di/AppModule.kt package com.yourcompany.app.diimport android.content.Context

import com.yourcompany.app.RecipeApplication // Or just pass Context
import dagger.Module
import dagger.Provides
import javax.inject.Singleton

@Module
object AppModule {
    // This is often provided via @BindsInstance in the AppComponent.Factory
    // If not, you can provide it here from the Application instance.
    // @Singleton
    // @Provides
    // fun provideApplicationContext(application: RecipeApplication): Context {
    //     return application.applicationContext
    // }
}

Step 4: Configure a Feature Module (e.g., :feature_example)

  1. Add Dependency on :core: In :feature_example/build.gradle:
// :feature_example/build.gradle plugins { id 'kotlin-ksp' }dependencies {
    implementation project(":core")
    // ... other feature dependencies
    implementation "com.google.dagger:dagger:2.x" // Use your Dagger version
    ksp "com.google.dagger:dagger-compiler:2.x"
}

Important: Do NOT add implementation project(":app") here.

  1. Define the Feature Component: This component will provide dependencies specific to this feature and will take CoreDependencies as a component dependency.
// :feature_example/src/main/java/com/yourcompany/feature_example/di/FeatureExampleComponent.kt package com.yourcompany.feature_example.diimport android.app.Activity

import com.yourcompany.core.di.CoreDependencies
import com.yourcompany.feature_example.presentation.ExampleFragment
import dagger.BindsInstance
import dagger.Component

// @javax.inject.Scope // Define FeatureScope if used
// @kotlin.annotation.Retention(AnnotationRetention.RUNTIME)
// annotation class FeatureScope

@FeatureScope // Optional: Define a custom scope for this feature
@Component(
    dependencies = [CoreDependencies::class],
    modules = [FeatureExampleModule::class]
)
interface FeatureExampleComponent {

    fun inject(fragment: ExampleFragment)

    @Component.Factory
    interface Factory {
        fun create(
            @BindsInstance activity: Activity, // If needed by your feature modules
            coreDependencies: CoreDependencies
        ): FeatureExampleComponent
    }
}

You’ll need to define @FeatureScope if you use it:

// :feature_example/src/main/java/com/yourcompany/feature_example/di/FeatureScope.kt (or in :core) package com.yourcompany.feature_example.di // or com.yourcompany.core.diimport javax.inject.Scope

@Scope
@Retention(AnnotationRetention.RUNTIME)
annotation class FeatureScope
  1. Define Feature-Specific Dagger Modules:
// :feature_example/src/main/java/com/yourcompany/feature_example/di/FeatureExampleModule.kt package com.yourcompany.feature_example.di// import com.yourcompany.feature_example.data.ExampleRepositoryImpl
// import com.yourcompany.feature_example.domain.ExampleRepository
// import com.yourcompany.feature_example.domain.ExampleUseCase

import dagger.Module
import dagger.Provides
import retrofit2.Retrofit // For example

// Placeholder classes for domain/data layer
interface ExampleRepository
class ExampleRepositoryImpl(retrofit: Retrofit) : ExampleRepository
class ExampleUseCase(repository: ExampleRepository)

@Module
object FeatureExampleModule {

    @FeatureScope // Optional
    @Provides
    fun provideExampleUseCase(repository: ExampleRepository): ExampleUseCase {
        return ExampleUseCase(repository)
    }

    @FeatureScope // Optional
    @Provides
    fun provideExampleRepository(retrofit: Retrofit /* Comes from CoreDependencies */): ExampleRepository {
        // Retrofit is accessible because CoreDependencies is a dependency of FeatureExampleComponent
        return ExampleRepositoryImpl(retrofit)
    }
    // ... other providers specific to this feature
}
  1. Inject Dependencies into Your Fragment/Activity in the Feature Module:
// :feature_example/src/main/java/com/yourcompany/feature_example/presentation/ExampleFragment.kt package com.yourcompany.feature_example.presentationimport android.content.Context
import android.os.Bundle
import android.util.Log
import android.view.View
import androidx.fragment.app.Fragment
import com.yourcompany.core.di.CoreDependenciesProvider
// import com.yourcompany.feature_example.R // Create a dummy layout if needed
import com.yourcompany.feature_example.di.DaggerFeatureExampleComponent
import com.yourcompany.feature_example.di.ExampleUseCase // Use the one from the module
import javax.inject.Inject

class ExampleFragment : Fragment(/*R.layout.fragment_example*/) { // Add layout if you have one

    @Inject
    lateinit var exampleUseCase: ExampleUseCase

    override fun onAttach(context: Context) {
        super.onAttach(context)

        val coreDependenciesProvider = requireActivity().application as? CoreDependenciesProvider
            ?: throw IllegalStateException("Application must implement CoreDependenciesProvider. Did you forget to implement it in your Application class in the :app module?")

        DaggerFeatureExampleComponent.factory()
            .create(
                activity = requireActivity(),
                coreDependencies = coreDependenciesProvider.provideCoreDependencies()
            )
            .inject(this)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        // Now you can use exampleUseCase
        Log.d("ExampleFragment", "Injected UseCase: $exampleUseCase")
        // exampleUseCase.doSomething()
    }
}

Step 5: Build and Run

  1. Sync your Gradle files.
  2. Perform a Build > Clean Project.
  3. Then, perform a Build > Rebuild Project.
  4. Run your application. Your feature module should now be able to access shared dependencies from the :app module via the :core module without a direct dependency.

Key Benefits Achieved

  • Decoupling: Feature modules are decoupled from the :app module.
  • Scalability: Easier to add or remove feature modules.
  • Build Times: Potentially faster build times as changes in one feature are less likely to affect others or the app module directly through this dependency path.
  • Dynamic Feature Compatibility: This pattern is essential for dynamic feature modules, which cannot depend on the :app module.

This detailed guide should provide a solid reference for your personal journal on setting up a robust, multi-module Dagger 2 architecture!


External references: