Navigation in multi-module Android project

Oscar Caballero
Ninety Nine Product & Tech
4 min readMay 28, 2021

--

Photo by Aaron Burden on Unsplash

These days design patterns like MVP or MVVM are quite common in Android development, providing a clean separation between the logic in the view and its lifecycle. Navigation between screens has now become an interesting challenge, with the decision about where the user should navigate to being taken outside the view, but performing this navigation the View due to the requirements of the Android framework.

Here in Ninety Nine, each feature is a project module, without going too much in deep into the architecture we apply, we only comment that the presentation layer implements the MVVM pattern.

Putting it simply, it looks like this:

Each of these modules implements what we call a flow, this is a user story that includes flow-through several screens that must be taken as a fixed set where navigation happens from start to finish.

That said, we want to make “available” that flow from different parts of the app, so a user can navigate to the starting point. For example, a user who has not yet logged in may want to be taken to the login flow from wherever they are in the application.

Let’s get started: the problem to solve

This solution for navigations comes with a number of requirements, and we will now explain how the Ninety Nine Android team solved them.

The requirements are:

  • API must be clear, with a correct semantic and self-documented. Any developer should be comfortable working with it and understand how to use it.
  • Evolution of the API should be manageable and secure, adjusting to the open/close principle.
  • Abstraction from the Android framework and from implementation details of code behind the scenes.
  • Support for multi-module projects, without giving unnecessary visibility between modules.

The solution

In the first place, we needed to define the set of possible screens (for our case the starting point of a flow) to which to navigate. We decided to not use an enumeration since it does not provide the flexibility of having a system to declare the requirement to navigate to a concrete screen.

The :feat-base module contains the pieces used to build each project feature, and it’s contained in each of them. It includes the set of screens by using a sealed class.

This way we are able to expose all possible destinations and their data requirements.

This set does not necessarily represent a set of Android screens (what is known as Activities or Fragments), they can also be a dialog, screens from other applications, external URLs, etc. In the end, we are talking about a list of possible destinations even outside the Android framework.

At the same level as :feat-base we created an abstraction by using the Command pattern, that allows us to implement in each feature the corresponding screens and in the project top level the navigator provider. This allows us to abstract the implementation details of specific screens, and by using this provider complies with the open/close principle.

Now that all our modules have visibility into the Navigator abstraction, we can implement in each of them the required navigations. To keep things clean and separated from the Android framework, the implementations of these concrete navigations are Kotlin objects at the same level as the screen to navigate to.

We only need one common navigator provider for the whole project, to be used through all the flows screens. This is implemented in the :app module, which has visibility into all screens.

Finally, we only have to perform navigation, where the Viewmodel emits to the view the need to navigate so that it can obtain the corresponding navigator and execute it.

At this point we came across one more problem: due to how the Android ViewModel framework works, the value emitted by them was observed by the View every time it is refreshed.

Thanks to this Medium post by someone at Google we found interesting solutions for these events that are only consumed once, by using LiveData with a data wrapper:

Thanks of the dependency injector we satisfy the navigator provider in the View, that we use to resolve the navigation to a screen emitted by the ViewModel:

You can find a complete example of how this solution would look like here:

Conclusions

We have shown how we have taken our first steps in a navigation system that allows us to escalate and at the same time keep things simple, so any developer in the team can easily understand and build onto existing code.

This escalation part is very important at Ninety Nine, allows us not to get into anti-patterns, and keeps things agnostic from the Android framework.

Hope you enjoyed this post! If you did, check our open positions in our careers page https://ninetynine.com/unete-al-equipo

--

--