Divide and Rule: Navigation Component in a multi-module project

Anton Uryvskii
5 min readJan 20, 2021

In this story, you will get to know how to organize graphs of each separate module / feature / user story, centralize them, build direct navigation between them, and use it with a Safe Args plugin.

You are now in the third story of my experience with the Navigation Component in a multi-module project. If you do not understand a single word above, then you better to get about:

Well, if you are already familiar with this library, then there is a nice bonus for you in the next story — an approach to organizing iOS-like multistack navigation.

First, let’s see how the project is divided into modules in all projects in my team (magora-systems.com):

  1. : app — the main module and entry point to the app. It should know about all the modules involved in the application.
  2. : core-the module contains all the basic things: base classes, models, entity, DTO, extensions, etc.
  3. Utility modules are used to encapsulate the functionality of the main components of the application. For example, working with the network, database, or the same navigation.
  4. Feature-modules contain the operation of a specific feature / user story, whether it is a flow or a screen.

Well, let’s make navigation with the Safe Args plugin connected.

When using a single graph, it looks like this:

It looks confusing and not obvious. To prevent this, you need to perform a number of actions:

  1. Organize the navigation graphs for each feature module.
  2. Select a separate Top-level graph.
  3. Make transitions between graphs of the modules.
  4. Determine where to store the Top-level graph.

Now let’s go through each of them in more detail.

Organizing the navigation graphs for each feature-module

To avoid problems with navigation between destinations within one module, we will make a separate graph for each feature-module and there we will specify all destinations and actions between them. With this approach, the navigation of the feature is completely encapsulated inside the module, and Safe Args classes belonging to the appropriate graph are generated in it.

Selecting a separate Top-level graph

The top-level graph is the one that is the starting point of navigation of the application and contains all the necessary feature-graphs between which navigation takes place.

It does not look so impressive, but it is effective.

Making transitions between graphs of the modules

To be able to navigate between the graphs of different features, you need to bring to each global action.

Deciding where to store the Top-level graph

There are several options, each with its own pros and cons:

  1. Base module (:core)

− Most modules are depending on it, but not all.

− It doesn’t know about any modules, so it won’t be able to see the graphs. It is worth noting that the application will compile and run despite Lint-a errors, but when you merge resources, everything still merges together.

− If you change the graph, you will need to rebuild the project, and since many modules depend on it, they would also need to be rebuilt.

  1. Application module (: app)

+ Knows about absolutely all modules.

− No module knows about it.

− Safe args will generate global actions in a place inaccessible to feature modules, so we won’t be able to navigate between graphs.

The results are not very rosy, but we have a whole plus in favor of the app module! So let’s leave the Top-level graph in it and deal with the minuses of this solution.

Minus: it does not know about any modules

Solution: make a separate module (:navigation), which will be known to absolutely all modules that will participate in the navigation.

Add all global action IDs to it. This way, generated files will understand what they are working with and will have access to the id of each global transition.

<item name=”action_global_nav_sign_in” type=”id”/>
<item name=”action_global_nav_sign_up” type=”id”/>
<item name=”action_global_nav_home” type=”id”/>
<item name=”action_global_nav_users_list” type=”id”/>
<item name=”action_global_nav_user_details” type=”id”/>
<item name=”action_global_nav_on_boarding” type=”id”/>
<item name=”action_global_nav_settings” type=”id”/>
<item name=”action_global_to_faq” type=”id”/>

Minus: the generated Directions and Args are located in the :app module

Safe Args will generate Directions and Arguments classes for the top-level graph in a place inaccessible to feature modules, so we won’t be able to navigate between graphs.

Solution: migrate and modify the generated files. It’s a little more complicated and there is a need to add build scripts. Generated classes are located in the “generated“ folder of the module where the graph is located (now it is :app) and there is no way to use them from :navigation module. Therefore we can use a small trick: during the build wait for the end of the work tasks generateSafeArgs, move all generated files in the :navigation module and, add an import of our navigation module Args and Directions-classes still use the R file of the module :app.

ext {
argsPath = ‘/build/generated/source/navigation-args’
appNavigation =“${project(‘:app).projectDir.path}$argsPath”
navigationPath=“${project(‘:navigation’).projectDir.path}”
+“$argsPath”
navigationPackage = “com.example.navigation”
}
tasks.whenTaskAdded { task ->
if (task.name.contains(‘generateSafeArgs’)) {
task.doLast {
fileTree(appNavigation)
.filter {
it.isFile() && it.name.contains(“Directions”)
}
.forEach { file ->
if (file.exists()) {
def lines = file.readLines()
lines = lines.plus(2, “import $navigationPackage.R”)
file.text = lines.join(“\n”)
}
}
}
move(file(“$appNavigation”), file(“$navigationPath”))
}
}

Summary

As a result, we get a single flow of work, in compliance with which navigation in the application is decomposable, scalable and run like clockwork. Even if you need to re-use some part of the graph in the project, you can put it in a separate module, make a separate graph and re-use it as you please.

Among other things, we have solved the problem with the files created by the plugin — now the rebuild in different flavors and types will not bother us, as it would be with a simple addition of sourceSet-s to the navigation module.

I didn’t stop at this victory and decided to see what else I could do with it. To do this, the project came in handy as never before, in which the customer wanted an application with a bottom navigation menu and that each tab kept its state when leaving it. This is the final story about iOS-like multistack navigation.

--

--