Multi-module Navigation with Architecture Components

It is possible to remove all Gradle inter-feature dependencies when you declare each feature nav graph ID explicitly in the base feature. I am not 100% satisfied with this solution since these IDs create "hidden" inter-feature dependencies but otherwise it works fine.

Here are the key parts of this setup:

:app

build.gradle

dependencies {
    implementation project(':features:feature-base')
    implementation project(':features:feature-one')
    implementation project(':features:feature-two')
}

:features:feature-base

build.gradle

dependencies {
    application project(':app')
    feature project(':features:feature-one')
    feature project(':features:feature-two')
}

navigation/feature_base_nav_graph.xml

<navigation ...>
    <include app:graph="@navigation/feature_one_nav_graph" />
    <include app:graph="@navigation/feature_two_nav_graph" />
</navigation>

values/feature_base_ids.xml

<resources>
    <item name="feature_one_nav_graph" type="id" />
    <item name="feature_two_nav_graph" type="id" />
</resources>

:features:feature-one

build.gradle

dependencies {
    implementation project(':features:feature-base')
}

navigation/feature_one_nav_graph.xml

<navigation
    android:id="@id/feature_one_nav_graph"
    ...>

    <fragment
        android:id="@+id/oneFragment"
        ...>
        <action
            android:id="@+id/navigateToFeatureTwo"
            app:destination="@id/feature_two_nav_graph"
            ... />
    </fragment>

</navigation>

navigate

findNavController().navigate(R.id.navigateToFeatureTwo)

:features:feature-two

build.gradle

dependencies {
    implementation project(':features:feature-base')
}

navigation/feature_two_nav_graph.xml

<navigation
    android:id="@id/feature_two_nav_graph"
    ...>

    <fragment
        android:id="@+id/twoFragment"
        ...>
        <action
            android:id="@+id/navigateToFeatureOne"
            app:destination="@id/feature_one_nav_graph"
            ... />
    </fragment>

</navigation>

navigate

findNavController().navigate(R.id.navigateToFeatureOne)

One of the approaches that might be useful is to create a completely new independent module (e.g ":navigation" module) and move all navigation.xml files from all other modules to it. Then we depend on that new (":navigation") module in all other modules where navigation related stuff is needed, and we will be able to access its R.navigation or generated argument classes, etc.

Since the new (":navigation") module doesn't know about anything else in the project IDE will mark red any fragment, activity and other classes we use in navigation.xml files, that are defined outside in other modules but as long as we use full class names (com.exampel.MyFragment) it will compile and work.

<?xml version="1.0" encoding="utf-8"?>
<navigation 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_graph_id"
    app:startDestination="@id/some_navigation_id">

    <fragment
        android:id="@+id/some_navigation_id"
        android:name="com.exampel.MyFragment".../>
        // com.exampel.MyFragment will be marked red since IDE can't link it
        // to the existing class because it is in the other module

This creates "hidden" dependency to all classes we want to navigate to in a way that we need to know class names and potentially arguments, and we have to maintain it manually but it allow us to easily separate navigation in independent module.


This is already a year-long but the library now can support this exact use-case! As of 2.1.0-alpha03, we can navigation through deep link URIs.

Instead of adding the features as implementation details to each other, we can leave them unaware between themselves and use deep link navigation.

Feature 1 - Detail - build.gradle

dependencies {
    implementation project(':base')
}

Same with Feature 2 - Detail. No need for it to know the other modules.

To have inter-module navigation, we have to first define the deep link for navigating through that destination via a deepLink tag.

Feature 1 - Detail - Navigation Graph

<navigation ...
    android:id="@+id/graph_feature_1_detail_id">
    <fragment ...
        android:id="@+id/nav_feature_1_detail">
        <deepLink app:uri="myApp://feature1detail"/>

    </fragment>
</navigation>

Feature 2 - Detail - Navigation Graph

<navigation ...
    android:id="@+id/graph_feature_2_detail_id">
    <fragment ...
        android:id="@+id/nav_feature_2_detail">
        <deepLink app:uri="myApp://feature2detail"/>

    </fragment>
</navigation>

Now that we have deep links with URIs set, we can directly use this in a NavController

So in the fragment in Feature 1 - Detail, maybe on a button click? Anywhere where you have to perform navigation

class Feature1DetailFragment {
   fun onViewCreated(...) {
       ...
       view.setOnClickListener {
           val uri = Uri.parse("myApp://feature2detail")
           findNavController().navigate(uri)
       }
   }
}

And in Feature 2 - Detail,

class Feature2DetailFragment {
   fun onViewCreated(...) {
       ...
       view.setOnClickListener {
           val uri = Uri.parse("myApp://feature1detail")
           findNavController().navigate(uri)
       }
   }
}

And voila! Inter-module navigation.

At the time of writing, the latest stable release is 2.1.0-rc01.

Although I haven't tried this out on more complex projects, I love this library and I'm hoping to see this library mature more!

I created a Medium article about this. You can take a look at it. Cheers!