Advanced digital products are almost always created as multi-module projects. It is easier to separate logic and shorten build times. There might be some issues with dependencies that can be optimized.

The challenge: dependencies

When having multi modules we have to think about dependencies. Otherwise, instead of a graph with different branches, we will see a list of dependencies. This means we will not benefit from faster build times, because the Gradle plugin has to build every module each time and can’t parallel the work.

Let’s analyze the above example graph.

We have 3 feature modules and we have to navigate between them.

When we want to follow navigation from the dashboard, through a screen to app version screen, we need dependencies of fragments classes. This is the root of our problem. Our graph is not optimized anymore.

The solution example: navigation logic

In order to optimize build times, we have to shorten dependency chains or shorten build times for given modules. Application module has always been dependent on all needed modules. It is a great opportunity to implement navigation logic between feature modules. This will cause our feature modules to stop being dependent on each other.

Declare interfaces

In order to get rid of dependencies, we will use Dagger for dependency injection. Let’s create an interface that is needed to create navigation in the about-navigation module.

interface AboutNavigation {
    fun navigateToAboutScreen()

As you can see, we don’t need implementation, therefore we don’t have a dependency on about-module. Class is lightweight and can be built quickly by Gradle. So it is an improvement over building an about-module. Let’s create an interface for app-version navigation in the same manner as the app-version-navigation-module.

interface AppVersionNavigation {
    fun navigateToAppVersionScreen()

Implement interfaces

As said previously, we can take advantage of the app-module, as it has dependencies on all modules. Let’s create implementations of our navigation interfaces in the app-module:

class AboutNavigationImpl @Inject constructor() : AboutNavigation {
    override fun navigateToAboutScreen() {
        // TODO add navigation code
        // We can, for example, use AboutFragment, as we have a dependency to about-module

class AppVersionNavigationImpl @Inject constructor() : AppVersionNavigation {
    override fun navigateToAppVersionScreen() {
        // TODO add navigation code
        // We can, for example, use AppVersionFragment, as we have a dependency to app-version-module

Okay, we have now methods to navigate between modules separated. But how to deliver implementation without recreating bad dependency chains?

Dependency injection to the rescue

We will use Dagger 2 and its binding feature to deliver an implementation of interfaces. In app-module:

interface NavigationBindingsModule {
    fun bindAboutNavigationImpl(aboutNavigationImpl: AboutNavigationImpl): AboutNavigation

    fun bindAppVersionImpl(appVersionNavigationImpl: AppVersionNavigationImpl): AppVersionNavigation

Then we can add this module to our Dagger’s ApplicationComponent.

Let’s assume we have created a Router class for managing navigation. We want to declare constructor injection with a given interface for navigation.

In dashboard-module:

class DashboardRouter @Inject constructor (aboutNavigation: AboutNavigation) {
    fun navigateToAboutScreen() {

In about-module:

class AboutRouter @Inject constructor (appVersionNavigation: AppVersionNavigation) {
    fun onAppVersionClicked() {

As you can see, we got rid of about-module and app-version-module dependencies. Instead, we have dependencies on navigation modules. They are lightweight because they have only interfaces to build, which is much faster than creating whole feature modules.


Do you want to find out more about Android?

Talk to us!

Dependency management in multi-module projects

It is easy to forget about dependency management in multi-module projects. But it is not so hard to fix and optimize. Create interfaces in separate modules for any functions or features that we want to get from other modules. Then inject an implementation of those interfaces by dependency injection. Note that the example given above is just an example of how to optimize dependencies. It doesn’t have to be navigation.

If you want to learn more about Android development at Applover, go to our blog!

Useful links for multi-module project

  1. Preferred way to organize navigation for multi-module projects.
  2. More detailed information about multi-module build times.
  3. Presentation about optimizing modules.
  4. Example of multi-module optimization.
  5. Gradle plugin that can help with finding dependencies worth optimizing.