Using Dagger in a Multi-Module Project

Using Dagger in our Android project helps us to manage dependencies in a very easy way.

So, a basic way to build our app is to have a single module called app and we have all of our code in the same package.

But, now let us say we are restructuring our app and designing our app using multi-module architecture. In this, we need to manage dependencies using Dagger as well.

In this blog, we are going to talk about how we can use Dagger for dependency management in a multi-module project.

We are going to learn,

  • Dagger and its basics.
  • Designing a multi-module app.
  • Adding dagger to your multi-module architecture.

Dagger and it's basic.

Dagger is a dependency injection framework for any project using Java/Android. For understanding the blog, you should have an idea about,

  • Module
  • Component
  • Scopes

If you want to get started with Dagger, click here

Designing a multi-module app.

Designing a multi-module app can be broken down into modules based on some features or services. In this blog, we would have 4 modules to start off with.

One would be,

  • app: Here app is the package created when creating the project.
  • base: It would be a module containing the common code for the project
  • feature-one: It would also be a module containing the code for a specific feature.
  • feature-two: It would also be a module containing the code for another feature.

All the modules will be implemented in the app's build.gradle like,

implementation project(':base')
implementation project(':feature-one')
implementation project(':feature-two')

and both the feature modules also depend on the base module and will implement it in both the feature's build.gradle file like,

implementation project(':base')

This above figure shows the project structure and how we have structured the project.

To get an understanding and learn how to build an app in multi-module architecture, click here.

Adding dagger to the app.

Now, let us start setting up dagger in our project.

First, let's setup out a base module. So, for that, we will add the dagger dependency into the base build.gradle file,

implementation "com.google.dagger:dagger:2.27"
kapt "com.google.dagger:dagger-compiler:2.27"

Now, we are going to create a package di and it will have a BaseComponent and BaseModule. And we also need to provide network and database service from the base component itself.

Our BaseComponent would look like,

@Singleton
@Component(modules = [BaseModule::class])
interface BaseComponent {

    fun inject(app: Application)

    fun getNetworkService(): NetworkService

    fun getDatabaseService(): DatabaseService

}

and the BaseModule looks like,

@Module
class BaseModule {

    @Provides
    @Singleton
    fun provideDatabaseService() =  DatabaseService()

    @Provides
    @Singleton
    fun provideNetworkService() = NetworkService()
}

Here, the BaseComponent would be provided as dependencies for other modules like the app and its feature-module

Let's design the first feature,

Before starting, first, let us add the dependency to both the features like,

implementation "com.google.dagger:dagger:2.27"
kapt "com.google.dagger:dagger-compiler:2.27"

First, we will create FeatureOneActivity.

Now, inside di package, we will create FeatureOneComponent which would get its dependency from BaseComponent like,

@FeatureOneScope
@Component(
    dependencies = [BaseComponent::class],
    modules = [FeatureOneModule::class]
)
interface FeatureOneComponent {

    fun inject(activity: FeatureOneActivity)

}

and we will also create FeatureOneModule like,

@Module
class FeatureOneModule {
}

Here, you can see out FeatureOneComponent is scoped with FeatureOneScope.

This scoping means that the Component would be used inside the feature-one module. The FeatureOneScope looks like,

@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class FeatureOneScope

Similarly, for feature-two module we will quickly setup the required class, activity, and interface required.

It would also have a module, component, and scope.

FeatureTwoComponent would look like,

@FeatureTwoScope
@Component(
    dependencies = [BaseComponent::class],
    modules = [FeatureTwoModule::class]
)
interface FeatureTwoComponent {

    fun inject(activity: FeatureTwoActivity)

}

FeatureTwoModule looks like,

@Module
class FeatureTwoModule {
}

and as you can see, FeatureTwoComponent is scoped to FeatureTwoScope which looks like,

@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class FeatureTwoScope

Now, let us setup the Application layer, with MainActivity and its Component and module. The app module will also have its application class.

MainActivityComponent would look like,

@MainActivityScope
@Component(
    dependencies = [BaseComponent::class],
    modules = [MainActivityModule::class]
)
interface MainActivityComponent {

    fun inject(activity: MainActivity)

}

MainActivityModule would be,

@Module
class MainActivityModule {

}

and it would have its own scope as well like,

@Scope
@Retention(AnnotationRetention.SOURCE)
annotation class MainActivityScope

We are done here setting up the modules with Dagger.

Now, to use the DatabaseService and NetworkService across all modules would require us to add dagger inside the Activity file as well.

In MainActivity we will update the code like,

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseService: DatabaseService
    @Inject
    lateinit var networkService: NetworkService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        DaggerMainActivityComponent
            .builder()
            .baseComponent(DaggerBaseComponent.builder().build())
            .build()
            .inject(this)

    }
}

where I am injecting the databaseService and networkService.

Similarly, we will update the code for FeatureOneActivity like,

class FeatureOneActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseService: DatabaseService

    @Inject
    lateinit var networkService: NetworkService


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_feature_one)

        DaggerFeatureOneComponent
            .builder()
            .baseComponent(DaggerBaseComponent.builder().build())
            .build()
            .inject(this)

    }
}

and for FeatureTwoActivity like,

class FeatureTwoActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseService: DatabaseService
    @Inject
    lateinit var networkService: NetworkService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_feature_two)

        DaggerFeatureTwoComponent
            .builder()
            .baseComponent(DaggerBaseComponent.builder().build())
            .build()
            .inject(this)

    }
}

Now, let us put a Log statement in MainActivity and FeatureOneActivity and start FeatureOneActivity from MainActivity.

In MainActivity's onCreate will add,

Log.d("DaggerSample_Main", databaseService.toString())
startActivity(Intent(this, FeatureOneActivity::class.java))

and similarly for FeatureOneActivity as well, will add,

Log.d("DaggerSample_FeatureOne", databaseService.toString())

Now, when we run the app we see in the Logcat,

DaggerSample_Main: com.mindorks.dagger.multi.module.base.data.DatabaseService@63d3f1
DaggerSample_FeatureOne: com.mindorks.dagger.multi.module.base.data.DatabaseService@e4bb6c2

Here, you can see two different memory address is allocated for the dabaseService when injecting in MainActivity and FeatureOneActivity respectively.

But we want to share the same instance across the application. In this case, using @Singleton would also not be of any success.

The reason here is, we are creating a new instance of BaseComponent every time we are passing it as a dependency to other Components like,

DaggerBaseComponent.builder().build()

Solution:

We need to share the instance of BaseComponent across the application so that we don't create a new instance of BaseComponent whenever we have to use it.

For that, we will create an interface in our base module like,

interface BaseComponentProvider {

    fun provideBaseComponent(): BaseComponent

}

Now, this interface would be implemented by the Application class which we created in our app module like,

class MyApplication : Application(), BaseComponentProvider {


    override fun onCreate() {
        super.onCreate()
   
    }

    override fun provideBaseComponent(): BaseComponent {
    }

}

In this, we need to return baseComponent instance. So, to return the instances of baseComponent in provideBaseComponent function, we will update the code as,

class MyApplication : Application(), BaseComponentProvider {

    lateinit var baseComponent: BaseComponent

    override fun onCreate() {
        super.onCreate()

        baseComponent = DaggerBaseComponent
            .builder()
            .baseModule(BaseModule())
            .build()
        baseComponent.inject(this)
    }

    override fun provideBaseComponent(): BaseComponent {
        return baseComponent
    }

}

Here, we created the baseComponent variable and returned it from provideBaseComponent function.

Now, to share the baseComponent to other modules, we will create an InjectUtils singleton in the base module that will have a function which returns BaseComponent like,

object InjectUtils {

    fun provideBaseComponent(applicationContext: Context): BaseComponent {
        return if (applicationContext is BaseComponentProvider) {
            (applicationContext as BaseComponentProvider).provideBaseComponent()
        } else {
            throw IllegalStateException("Provide the application context which implement BaseComponentProvider")
        }
    }

}

Here, we created a function provideBaseComponent which takes the context as a parameter.

In this, we would be using it in other activities, which have their components dependent upon BaseComponent.

So, in InjectUtils, we check if the applicationContext provided by the other modules implements the BaseComponentProvider or not. If yes, we get the baseComponent and then use it with other modules else we throw an exception.

Now, in all the activities we update our code. In MainActivity we update code like,

class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseService: DatabaseService
    @Inject
    lateinit var networkService: NetworkService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        DaggerMainActivityComponent
            .builder()
            .baseComponent(InjectUtils.provideBaseComponent(applicationContext))
            .build()
            .inject(this)
        Log.d("DaggerSample_Main", databaseService.toString())
        startActivity(Intent(this,FeatureOneActivity::class.java))

    }
}

In FeatureOneActivity, we update the code like,

class FeatureOneActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseService: DatabaseService

    @Inject
    lateinit var networkService: NetworkService


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_feature_one)

        DaggerFeatureOneComponent
            .builder()
            .baseComponent(InjectUtils.provideBaseComponent(applicationContext))
            .build()
            .inject(this)
        Log.d("DaggerSample_FeatureOne", databaseService.toString())

    }
}

and similarly, for FeatureTwoActivity, we update the code like,

class FeatureTwoActivity : AppCompatActivity() {

    @Inject
    lateinit var databaseService: DatabaseService

    @Inject
    lateinit var networkService: NetworkService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_feature_two)

        DaggerFeatureTwoComponent
            .builder()
            .baseComponent(InjectUtils.provideBaseComponent(applicationContext))
            .build()
            .inject(this)
        Log.d("DaggerSample_FeatureTwo", databaseService.toString())

    }
}

In all of the activity, we can see we replaced the,

DaggerBaseComponent.builder().build()

with,

InjectUtils.provideBaseComponent(applicationContext)

Here, you can see we are passing the application context of the application. The application class has implemented the BaseComponentProvider interface which is helping us to return the baseComponent across other modules.

And now if we run the app and check the Logcat, we will see,

DaggerSample_Main: com.mindorks.dagger.multi.module.base.data.DatabaseService@63d3f1
DaggerSample_FeatureOne: com.mindorks.dagger.multi.module.base.data.DatabaseService@63d3f1

Here, you can see we now have the same memory address located to both the DatabaseService instance in the app's MainActivity and in FeatureOneActivity.

This means that the same instance is used across all the modules in the app.

Conclusion

So, to use dagger in a project which is multi-modular, we need to make sure that we pass the instance of BaseComponent across the app.

So, this is how you can use Dagger in a multi-module app architecture which will help us to manage dependency in an efficient way.

You find the complete code here.

Check out the free android tutorials by MindOrks.

You can learn how to use dagger in dynamic feature module here.

Happy learning.

Team MindOrks :)

Also, Let’s connect on Twitter, Linkedin, Github, and Facebook