Let's Navigate
Harrypulvirenti
Posted on March 8, 2021
As Android Engineers probably many of you have already approached the development of a multi-module project and, if not, please consider doing that an maybe check out this cool article by Joe Birch.
Modularizing Android Applications | by Joe Birch | Google Developer Experts | Medium
Joe Birch ・ ・
Medium
If you already had some experience with this topic, you probably know that one of the most challenging problems of a multi-module project is how to approach the navigation between various feature modules.
There are tons of possible approaches around this topic and here I'm going to talk about one based on Navigation Component from Google.
Suppose that you have this situation:
One Application Module and a few other feature modules.
At this point let's say that we want to navigate from feature 1
-> feature 2
.
The first thing that you maybe be tempted to do is to introduce one new dependency in the Gradle from feature 1
-> feature 2
in order to see the destination that you want to reach.
Well, this is not a good practice because later on time can happen that you will have the need to navigate back from feature 1
<- feature 2
and, once you introduced this new dependency, you will have a circular dependency in your project.
Now that we understood why it's so complex to navigate between different modules we can try to find some good solution to this problem with the help of Navigation Component.
In this post, I'm going to assume that you have, at least, a basic knowledge of what is Navigation Component and what is capable to do.
Trying to describe it in the simplest way possible, we can say that Navigation Component is a tool that allows you to declare all your destinations
and routes
in one XML
, called Navigation Graph
, and provides a high-level API to navigate between them.
One feature that, in this case, can help us a lot is the ability to split the content of one monolithic navigation graph into multiple ones.
Now that we have these concepts, let's summarize all the goals that we want to achieve with our navigation implementation:
- Every feature should be responsible for its own graph.
- Every feature should be able to reach all the available destinations inside the app.
- Avoid Circular Dependencies.
Well! Now it's finally time to code!
The first thing that we have to do is to declare all the Navigation Graphs
for every feature so we will end in a situation like this:
In this way, inside the main_graph.xml
, should be possible to do something like this:
<include app:graph="@navigation/feature_1_graph.xml" />
<include app:graph="@navigation/feature_2_graph.xml" />
<include app:graph="@navigation/feature_3_graph.xml" />
Doing so, every feature will be responsible for its own internal Navigation Graph
and should expose only the entry points needed to navigate to the feature.
All the sub-graphs
are then collected by the application and included inside the main_graph
to allow all the features to perform the navigation.
Now we have only another problem: How to perform the navigation?
In order to navigate with Navigation Component we need two things:
- The destination that we want to reach is included in the
main_graph.xml
. - The ID of the entry point of the destination.
The first point is done so we need to focus on the second one.
To solve this problem we can use one small trick:
Declare in advance all the entry points IDs in a shared place
destinations.xml
<resources>
<!--
This resource file is the place where you should declare public navigable destinations inside the app
-->
<item name="feature1Fragment" type="id" />
<item name="feature2Fragment" type="id" />
<item name="feature3Fragment" type="id" />
</resources>
In this way, we can use the predeclared IDs to later provide the implementation of the destinations and/or new routes that are using the IDs of the destinations.
feature_1_graph.xml
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/feature_1_graph"
app:startDestination="@id/feature1Fragment">
<fragment
android:id="@id/feature1Fragment"
android:name="com.navigation.sample.feature1.Feature1Fragment"
android:label="Feature1Fragment">
<action
android:id="@+id/feature1ToFeature2"
app:destination="@id/feature2Fragment" />
</fragment>
</navigation>
At this point your Gradle dependency graph should look like this:
Now let's check our goals list to see how many progress we did so far:
- Every feature should be responsible for its own graph. ✅
- Every feature should be able to reach all the available destinations inside the app. ✅
- Avoid Circular Dependencies. ✅
As you can understand from the goals list, the solution that we implemented is satisfying all the requirements that we fixed but there is one thing that we can still improve optionally.
This solution has one requirement:
Every time that we want to add or remove a feature we have to manually modify both the main_graph.xml
and the destinations.xml
to update the included features properly.
Not a big problem in the end but since developers are lazy we can try to find a better solution for this problem. 😝
But what can we do about it? 🤔
The idea here is to collect all the sub-graphs
references and include them programmatically to the main_graph.xml
at runtime.
To reach this result we are going to use the Dependency Injection
.
Here doesn't matter if you prefer to use Dagger2, Koin or whatever.
The important thing is that in every feature you should be able to expose the navigation graph reference of the feature outside of its own module.
Let's see a quick sample:
@Module
object Feature1GraphModule {
@Provides
@IntoSet
fun provideGraphReference() = R.navigation.feature_1_graph
}
Next, you should be able to access all these graphs references and include them in the main_graph
with a code like this:
val mainGraph = navController.navInflater.inflate(R.navigation.main_graph)
// subGraphsIDs is the set of all the sub-graphs collected by Dagger multi-binding
subGraphsIDs
.map { graphId ->
navController.navInflater.inflate(graphId)
}
.forEach { graph->
mainGraph.addAll(graph)
}
navController.graph = mainGraph
This operation should be performed in your main NavController
in order to allow the entire app to see your sub-graphs
.
Now, to wrap up let's check our Gradle dependency graph one last time:
As you can see now we have a new module called main_activity_feature
.
This is where your MainActivity
/Main NavController
should be located.
In this module, you should perform the setup that I described before.
Another small difference is that now the app
module doesn’t know anything about the main_graph
, about the navigation or anything else.
Now the app
module is depending on the feature modules only for collecting all the Dependency Injection Modules
and this is a big plus in terms of code maintenance because, in this way, every time you need to add or remove a feature you just need to provide the correct DI setup and you are done!
A working sample of this navigation approach can be found in my personal project here:
I hope that you liked this small post and thanks for reading till here!
Posted on March 8, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.