Android App Modularisation and Navigation

Júlio Cesar Bueno Cotta
VeepeeTech

--

TLDR:
We present a common problem when modularising Android Apps and the navigation library that we created to help us with it. Github repository here with a sample.

Introduction

In the Android development world we have many challenges and modularising the app is one of the most common. Any reasonable sized app today will have dozens or hundreds of modules. But, why to modularise you ask? Well, there are many reasons, like better code ownership (the code inside a module belongs to a team/feature), better separation of concerns (you can have modules for network logic, database, caching… ) and the ability to compile independent parts of the code in parallel thanks to our multi core machines (compilation speed).

Take a look at this simple dependency graph:

If you want to take advantage of those sweet, multi core compilation capabilities of your machine you need to make FeatureA and FeatureB modules compile independently of each other, but what if ActivityA needs to open ActivityB that is located on the FeatureB module? What if ActivityB needs to show FragmentA that is located on the FeatureA module? These are common scenarios, and it generates a circular dependency problem. The Link Router library was developed to solve this problem.

Features of Link Router
- Enables multi module navigation
- It has strongly typed destinations and parameters
- Supports Activities, Fragments and DeepLinks
- DeepLinks interception
- It can be set up in a distributed fashion

In this post we are going to focus on the multi module navigation aspect as it is the basis for the other features. You can take a look at our sample to learn more.

Before we continue

At Veepee, we know that Google recommends using a single Activity architecture, but this is a project with more than 7 years of history, so we have plenty of Activities. I would say that there are plenty of other projects out there that have the same architecture structure we do and it is not feasible to refactor everything.
The Link Router library was developed because of our needs, while modularising our project we found ourselves declaring public deeplinks on AndroidManifest files even though that Activity was not meant to be publicly accessible from the system.
We were also, not able to access Fragments from other modules and our previous DeepLink internal routing API had many flaws.
We tried to take a look at existing libraries, like Navigation Component from Google, but it seemed too complicated and it did not support multi-Activity scenarios, which was a requirement for us.

Back to Link Router

Link Router is a Service Locator for Intents, Fragments and DeepLink stacks. In the the next steps below we will give you a broad picture of how things work.
When the app starts up, we register all Activities, Fragments and DeepLink mappers in a Builder, and when the library needs to route anything, it just calls the right mapper and returns the stuff you need.

1) Register all mappers

Don’t worry about what those {Component}NameMappers are at the moment, we just need to register those before we are able to use route anything, so we do it in the Application’s create method.

2) Build the builder

Somewhere, after the registration of all NameMappers we need to build the Router.

3) Call the routing methods

So we can call Router methods to navigate.

Nice and simple, right?
Well, I left a few things without explanation. To keep the code organised, and to make it work we need a few abstractions, and a short setup.

Link Router Abstractions

The Routes module contains implementations (ActivityLink, ActivityName, FragmentLink, FragmentName) for your Activities and Fragments, it also contains Parameter implementations (ParcelableParameter) for each ActivityLink/FragmentLink that uses one. You may see this module as the public API for navigation to a given destination.

The Activity, Fragment and DeepLink mappers’ (ActivityNameMapper, FragmentNameMapper and DeepLinkMapper) implementations should live in the feature module that knows the logic to open that destination.

NOTE: We used the Routes module to simplify our example, but you could split that module into different modules, like FeatureARoutes and FeatureBRoutes for isolation and build caching.

Activities navigation

The basic flow when defining a defining a route to ActivityA is shown below in just 4 steps.

  1. Declare the ActivityName (Routes module)

We declare a constant/key (ActivityName) to represent an Activity class.

2) Declare the ActivityLink (Routes module)

An ActivityLink that represents something similar to an Intent.

3) Declare your ActivityNameMapper (featureA module)

ActivityNameMapper is responsible for mapping the ActivityName to an Activity class, so it needs to be declared in the module that contains the Activity class.

4) Register your ActivityNameMapper

Just like we did before, each NameMapper needs to be registered in the RouterBuilder.

NOTE1: Did you notice that the implementation of ActivityNameMapper is capable of handling multiple Activities in the same module? This was quite a common scenario when we started modularising our app.

NOTE2: Step 4 can be done with Dagger/Hilt or any other DI library that can aggregate multiple implementations of the interface. In our project we use the StartUp library to add our mappers to GlobalRouterBuilder.

Fragments navigation

Well, it is basically the same as for Activities - just replace ActivityNameMapper and ActivityName with FragmentNameMapper and FragmentName

DeepLinks navigation

When routing DeepLinks the library will select the right DeepLinkMapper based in the supported schemes and the authority, but you can override the method canHandle to add further checks.

  1. Declare the supported schemas by your application

Your app can have multiple DeepLink instances, maybe an https and a custom scheme? It is upon you.

2) Declare a DeepLinkMapper

Notice the code below will return an Array of ActivityLinks, that is the stack of Activities that will be opened when this DeepLink is triggered.

3) Register the DeepLinkMapper

Just like with the other examples, we register the mapper.

4) Route your deepLink

The method Router.route(Context, DeepLink) will start all Activities returned by DeepLinkRouter.stack(DeepLink) in reverse other.

Parameters

Suppose that your Activity requires a parameter, like an Id to be opened. What can we do?
When declaring your ActivityLink implementation, implement a ParcelableParameter, like below:

This will enable you to declare at ActivityB the following:

We have plenty of extension functions to help you extract the parameter from Bundles.

Troubleshooting/FAQ

Q: What happens if I forget of register a mapper?
A: The routing will fail at runtime with an exception telling you what ActivityName/FragmentName/DeepLink failed to be mapped.

Q: Can I have more than one Parameter?
A: No, but you can use Sealed Classes and you can add more fields to your Parameter.

Q: Is it possible to register the NameMappers in a distributed way?
A: Yes, it is. Use multi-binding from Dagger or StartUp library to register your mappers before building your router instance.

Q: Is it possible to intercept a DeepLink routing and change it’s destination?
A: Yes, it is. Take a look at our tests and sample in the repository.

Future work

Support Compose. We know, we know, Jetpack Compose is cool, new and shiny, but it is a whole new challenge so we need time to explore.

Final words

We hide the Activity/Fragment classes behind the ActivityName/FragmentName indirections. ActivityLink/FragmentLink together with ParcelableParameter provide type safe navigation and routing DeepLinks is just a composition of ActivityLinks. All of this makes sense and work together to deliver what we expect with a reasonable API.

We have been using the library internally for more than 8 months now without any big issue. Even though it takes a bit of code to enable the navigation, it is worth it. No code generation, no magic tricks. We recommend you to give it a try. Any feedback will be appreciated.

Github repository here.

Happy routing!

We are hiring

Shameless plug here. Come work with us and help Veepee deliver high quality apps. Take a look at the open positions at Lever.

--

--