ToDo example navigation graph
Base navigation architecture abstraction
First step was to come with a basic navigation abstraction simple enough to navigate back and forth. Simplest model we could think of was a navigation stack, where you can push/pop fragments as you go.
Resultant interface was about having a delegate Router component who will drive fragment’s navigation to destination Routes, being able to push/pop them.
Although this abstraction was made with Jetpack navigation in mind, you can easily change the implementation to use a different one.
Sending the instance allows us to identify the source route and also access the Jetpack’s navigation controller as well
Next step is to go into each of feature module’s fragments and define the specific Router and Routes required to work. Following the demo app example we have 4 fragments to work with (and you can check the navigation reference in the above image):
- TODOs List
- Add new TODO Form
- TODO details
- Delete TODO confirmation popup
To simplify the example we are going to just focus on the list and form. Resultant routers look like the following code:
Each of the Routers with destination routes has an IRoute
sealed class with all the possible destinations in it, with a push
function to be used. By having this we can identify and extend every specific source router push
function without collision issues.
Let’s look now at how our IRouter
and IRoute
abstractions are being used by the list Fragment in example:
In the Demo repository, we have some extra support classes such as StackFragment
and RoutedViewModelDelegate
to enable navigation from viewModels and improve code reusability at the same time.
LiveDataEvent refers to a single event to be triggered by a LiveData. This class can be found in the demo repository as well.
By having the injected router inside the fragment, we are able to navigate to available routes. Also consider that, by having routes defined in the Router definition, we are also able to let the navigations decisions up to the fragment’s viewModel.
Final step is to implement those abstractions with Jetpack and inject them. As we wanted to avoid Jetpack navigation dependencies in the feature modules or base library, we built all the implementations in the app module.
So how do we bind all these abstractions using Jetpack? let’s look at the example again:
Main idea is to create a Router implementation related to every nav_graph you have in your app and take advantage of all the support classes Android Jetpack Navigation provides for you.
This AppRouter
follows the navigation graph shown in the previous image, with all 4 fragment routers required implementations in it. As you can see, the only responsibility here is to map between Routes
and actual navDirections
from Jetpack.
Here are the guides on how to integrate Jetpack Navigation and Safe Args, and Hilt for Dependency Injection.
After having all these steps in place we are now ready to navigate inside our app!
Passing arguments in a safe way
First requirement we met was to be able to pass arguments between fragments. As Jetpack provides SafeArguments
support we wanted to take advantage of them. So let’s follow the steps again. First one was to provide a simple abstraction.
Each of the Routers with arguments to read extends IArgsRouter
implementation with the attached child Args
class inside.
An important note is we choose Serializable
as the base abstraction for arguments as it’s supported by AndroidX navigation, and it’s safe to use it in our domain classes as they are Kotlin pure modules.
To display the usage example let’s now focus at the navigation between List and Details from the Demo, as we need to send the Todo ID.
So first step is to enable arguments definition in the Routes
as the list does. We use data classes for these route definitions.
On the destination side, by having the injected router inside the fragment, we are able to retrieve fragment’s arguments if any as simple as calling its args()
getter, now available from the IArgsRouter
.
And as I told you at the beginning, implementation has no secrets as Jetpack provides all the support classes for you to use. The only thing to do is to map between Args
and SafeArgs
support classes
Here are the guides on how to enable Safe Args as well
Splitting navigation into subgraphs and it’s hidden secrets
One thing you may notice from the Demo example is that Details fragment lives in a different navigation graph. As your app grows in screens you may want to rearrange the navigation and use more than one navigation graph to improve visibility, and also to work with the navigation scope for view model’s lifecycle. In our team, as soon as we started doing it we noticed SafeArgs
were not available for actions between graphs unless you explicitly declared them in the action itself. This requirement brought us issues when updating arguments, as sometimes we forgot to update the related actions, and became a big pain. Lucky us, we found a useful trick to overcome this issue.
If you were paying attention in the previous section, to pass arguments to the details route, the AppRouter
implementation used DetailsNavGraphDirections
instead of TodoListFragmentDirections
. Now it’s the time to explain why.
A simple rule we started to follow was to declare a global action on each navigation graph to be used by other graphs when wanted to navigate into it. Yes, it means a global action with the navigation graph itself as destination. No trick here, but in terms of visibility, when the start destination of the graph updates its arguments, is easy to notice the global action must be updated as well, as it’s written just above that fragment definition.
Here comes the trick. After having this global action in place, we can use it on a different graph by only writing its id
and destination
. By using the same id
we are able to replace the fragment support action generated by Jetpack with the global one. As they share the same id, but only the global one contains the arguments definition, SafeArgs
will become available on that support class.
An example of this rule can be found in the demo app as well, buts let’s look at the important code here:
Yet again, the guides on how to start using nested graphs and global actions
Back Stack comes to help
Another problem we started to face was about passing data back as a result from a Fragment to its parent.
First attempt was to use shared view models to maintain data between them, but again, started to become a pain for us as we had to create too much boilerplate code maybe just for a single boolean result. But the back stack support came into Jetpack navigation, and we decided to give it a try.
First step again was to look for a simple abstraction. Best ones we can thought were pushForResult(Fragment, Route, callback)
, which will be used on the parent Fragment, and popWithResult(Fragment, result)
on the other side.
Implementation details are in BaseRouter
from the demo example of course. But the approach was to maintain a LiveData
in the Android BackStackEntry
as was available to use with the Jetpack Navigation.
You can find the full example in the navigation between the Details view and its Delete popup. But again, here comes the resume:
You can read more about the back stack in android navigation here
Final thoughts
A year has passed, and we are still working with Android Jetpack navigation over this Architecture that I tried to briefly explain. The real implementation has different names and some extra differences with the Demo one, like the lack of the included support classes. Of course, we do have some extra solutions to more specific problems that appeared on the way, but the goal of this post was to show the most usual ones.
At that time we really wanted to have some guidelines on how to integrate Jetpack navigation without being totally coupled to it. So hope this “guide” helps future devs looking for it.
More posts about our story at Sync. are yet to come. If you are interested at what we do, feel free to reach us here. We are always looking for new people to join in our journey.
Thanks for reading!