
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 theApplication
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
- If it doesn’t exist, create a new Android Library module named
core
.- In Android Studio: File > New > New Module… > Android Library.
- 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
- 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
}
- In the
:core
module, create an interface that yourApplication
class will implement to provide theseCoreDependencies
.
// :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
- 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")
}
- Define
AppComponent
in:app
: This is your root Dagger component. It should implement theCoreDependencies
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.
}
- Define Dagger Modules in
:app
(e.g.,AppModule
,NetworkModule
): These modules will provide the concrete implementations for the dependencies listed inCoreDependencies
.
// :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 providesContext
) and other modules provide all dependencies required byCoreDependencies
.
- Modify Your
Application
Class in:app
: ImplementCoreDependenciesProvider
and initializeAppComponent
.
// :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
)
- 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.
- 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
- 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
}
- 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
- Sync your Gradle files.
- Perform a Build > Clean Project.
- Then, perform a Build > Rebuild Project.
- 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: