Scalable navigation in Android programmatically with Jetpack Navigation
Jetpack Navigation is a great library and this is the library that enabled us to use fragments without thinking about FragmentManager and FragmentTransactions which were pain to work with.
One thing that I don’t understand about Android is that they make great libraries but somehow they find a way to tie the library with xml and ruin everything. It’s not like I hate xml but I believe things would be better off if we can do everything programmatically(Jetpack compose). With xml there is this hidden coupling between xml and code which is not very obvious at first. Once you start to scale (single codebase with multiple clients apps), you will hit the limitation of xml. In simple terms, xml is not dynamic, you can’t change the xml tags at runtime (maybe you can, let me know). Anyway, I am glad that Android is moving away from xml.
What is scalability for mobile apps?
Before we move any further, let’s try to answer this simple question, What is scalability for mobile apps?
We have heard so much about scalability in the context of backend development, where it basically means to scale the services to as many users as possible either by using architectures like microservices or by simply increasing servers to handle the load. In the context of mobile apps, it’s not like after 100000 users install the app your app will crash because it can’t handle any further new installs(your backend might crash, but that’s a different scenario).
In my understanding, scalability for mobile apps means scaling the app to clients (not to end users). If you have ever worked on an enterprise then you might be familiar with how they work. Enterprises are normally B2B (Business to Business) and don’t deal with end users directly. For example, in an enterprise that provide food ordering solutions to clients, the clients are the restaurants. With scalable architecture, you should be able to share the same codebase among all the clients and in the mean time allow customization to the app(like one restaurant might want this screens, other might want the users to route to different page when you click a button).
If you want your app to scale, it means your app should handle new clients without any significant changes to the codebase.
Sample
This is the app that we will be building. You can find the source code below.
https://github.com/susonthapa/programmatic-jetpack-navigation/tree/test-programmatic-navigation
Issues of Navigation with XML
- No dynamic configuration at runtime.
- Doesn’t play well with modular projects where you have to use a lot of dependency inversion.
- Safe-args sometime doesn’t work well with the IDE.
- Doesn’t scale well if you have tens of screens.
Custom Routing
In Android anything that you can do with xml can be done (almost) with code, in the end xml gets converted to intermediate format and then inflated at runtime. So, in this story, we will design a custom router that is going to handle the navigation for us. We will be able to customize the routes on the fly. I have done two implementation for this, one is using xml Ids and the other is using routes. I will be talking about routes as this is where Android is heading with Jetpack Compose but if you want to learn about the implementation using Ids then you can checkout this commit.
The big picture.
Lets break down the diagram.
Map of Fragments and Activities
We store all the fragments and activities that can be navigated within our app in a map
so that we can route dynamically and override routes we want at runtime. Here is a Screen
sealed class that is used to define all the screens that are present in the App. We also create FragmentScreen
and ActivityScreen
for fragments and activities respectively.
There is createRoute
function that returns a Pair of String (we can also create a separate class for this, but this is for the sake of simplicity) in which first is the id
that will be used to get the destination from the map
of screens and the second is the actual route with all the arguments.
Now, we need to setup mapping between the routes and the destinations in the Application
class.
Custom Router
This component finds the fragment or activities and routes to that. The idea is very simple as shown in the flowchart above. First we try to route to the destination if it is already present in the backstack. If that fails, we try to create a new destination and route to that otherwise we show an error screen. This is a very basic implementation and there are a lot of things that can be improved(like adding error screen for activity routing, or adding routing for dialogs).
Base Fragment
This is just a parent Fragment that exposes router to child Fragments and also handles parsing of the arguments, we can do the same for Activities as well. We can further improve this by adding error handling while parsing arguments so that if we don’t have the arguments that we need, we can route to custom error screen instead of crashing.
Setup with Bottom Navigation
We can programmatically setup our routing with Bottom Navigation. For that we can create the menu programmatically and then add the destinations to the graph and set it up with Bottom Navigation.
Use cases
I have listed couple of use cases that are very common in Android.
Simple Navigation
Routing to fragment or activities.
Navigation with arguments to fragment
We are using createRoute
function that takes the arguments needed by the route.
Navigation with Animations
Since the custom router is just a wrapper around Jetpack Navigation you can customize things supported by Jetpack Navigation.
Pop destination
A common use case is to pop the current destination when you navigate to some screen so that when the user presses the back button they don’t see the previous screen. This can be achieved like this.
Well, that’s a lot of code and if have made this far I believe you see the advantage that this approach brings and here are some of them.
- Dynamic Routing: We can change the mapping of our destination at runtime. Let’s say if you want to show a different screen for the same route based on response of the server, then you can. Just override the route in the map(in this implementation the map is immutable but you can change it to mutable or expose some ways to do that).
- Type-Safe: Although, we have
SafeArgs
plugin for xml this is obtained out of the box by usingcreateRoute
function. - Global error handling for route: Like in web where we can have custom error page for 404, we can achieve the same with this architecture.
- Easy to use: To add a new destination just add the route to the
map
and then use the route anywhere in the app. - Easy DI integration: You can easily add the router in your
core
module then setup the routing in theapp
module. After that any feature module can use routing to route to any screen without depending on the specific module.
That’s it for this story. I hope you have learned something useful. This architecture is very flexible and you can customize it easily.