Nested routes with Navigation 3

I

Iago Fucolo

Guest
1*cxW4YQy91zXQKfRp2L2NVA.png


As I could not find any guide for it, I have decided to do it on my own and share with you. But first it is important to check my previous article if you are not familiar with navigation 3 and want to understand how I moved from fragment navigation graph to navigation 3.


In the previous article, I replaced an entire flow using navigation 3, and after that I faced a challenge: how could I replace all the flows only using compose navigation?

I replaced a flow inside of activity, which was fine, but I donโ€™t want to have a separate activity for each flow. I want to use sub/nested routes components and end-up having only the MainActivity, with the rest of the app in compose.

Letโ€™s first take a look how the flows are into the app:

1*po-V88zHCv6LwbKB3iZL7A.jpeg

Sanctus App flows

As we can see, each main flow is an activity, using NavDisplay from navigation 3 to handle the flow itself. However, the goal here is no longer have flows in activities, but to have a concise flow using only compose.

How does it can be done?

There are many things that need to be changed. Letโ€™s list it here:

1- The bottom navigation bar still uses fragment to manage each tab. First I need to replace it with compose bottom navigation.

2- Replace Tab Fragments with Compose routes screens.

3- Replace Activities flow with Compose flows using Nav3.

1โ€Šโ€”โ€ŠBottom navigation bar​


Letโ€™s quickly check how it was with fragments:


XML of the MainActivity where the fragments and the Bottom Navigation are.


This is the setup for linking the BottomNavigationView with the navigation graph and fragments in order to show each tabโ€™s screen.

funny spot: findViewByIdโ€Šโ€”โ€Šwho remember that?

Each tab is a fragment: HomeFragment, PrayersFragment, PlansFragment and UserFragment.

HomeFragment:
can open PhraseOfTheDay, SaintOfTheDay, PrayersDetail and NovenaFlow, ChapletFlow and PlansFlow.

PrayersFragment:
can open PrayersDetail, NovenaFlow and ChapletFlow.

PlansFragment: one deep fragment that can access SubPlansFragment, and both can open PlansFlow.

UserFragment
: itโ€™s still under development and we are not discussing it in this article, as it does not go to any flow.

All flows are currently being opened as Activities, and we still have one nested Fragment in the Plans tab: SubPlansFragment.

Letโ€™s check out the blue print to start this migration:

Bottom navigation from material 3:

First stepโ€Šโ€”โ€Šcreating the routes:


Here we have our 4 main routes, and they are marked as @Serializable because it allows them to be converted to and from JSON. This is important for saving navigation state and passing route information between different parts of the appโ€Šโ€”โ€Šsuch as background processes or deep linkingโ€Šโ€”โ€Šwhere the route needs to be represented as data. They also extend NavKey, which is required for use with rememberNavBackStack.

And there is also the bottomNavItems, that is a list of the 4 navigations to be used in the navigation bottom bar.

Now that we have the foundation of the main routes in the app, we can check how it is implemented.


This is the bottom navigation bar. Iโ€™m using the bottomNavItems list to build it and, for each item, I get the necessary resources as Icon and text label. If the currentRoute is selected, it gets highlighted in the bottomNavigation. And when an item is clicked, it calls the higher-order-function onNavigation to request the navigation.

1*sXnJss5tTSW4238P_Mq-XA.png

How it was, and how it is with material 3

2- Replace Tab Fragments with Compose routes screens.​


Now, we are going to use RouteCompose Components to handle nested routes. But how? Letโ€™s first take a look at the code and explain it:


First, letโ€™s focus on the entryProvider from NavDisplay.

Each App route will open a RouteComponent, which has his own navigation stack, completed separate from the main navigation. Why is that?

Since this is a nested navigation, it is important for the RouteComponent to manage its own internal backstack to promote modularity and encapsulation. This prevents the main backstack from growing too complex and separating it makes it easier to maintain, as each RouteComponent is isolated from the main navigation flow.

Letโ€™s update the architecture flow:

1*1--seEyIuj3IuiiA1VWDUQ.jpeg

Fully compose navigation diagram

Now, all screens are being routed inside of the MainActivity, which is responsible only for the main navigation backstack, which is the 4 main routes of the bottom navigation.

Letโ€™s take a look into the HomePrayersRoute as an example:


HomeRouteComponent
is the only RouteComponent that can navigate to all flows and screens in the app. Since it is an isolated RouteComponent, it needs to manage its own backstack:


And for this we also need the HomeSubRoute, which holds all possible routes for this component.


With everything defined we can take a look in each Route:

HomeRoute, is the starting point and can navigate to any flow or screen in the app. When it is current route in the backstack, we call onShowNavigationBar to display the bottomNavigationBar. If any other screen is active, the navigation bar will be hidden (take a look at the MainContent to understand a bit more)

SaintRoute, PhraseOfTheDayRoute and PrayerRoute only navigate to a new screen in the backstack. These screens do not contain any nested RouteComponent.

LiturgyRoute, ChapletRoute, NovenaRoute and PlanRoute navigate to another RouteComponent, each of which manages its own internal navigation backstack.

Each Route is isolated and reusable in any navigation call, because it doesnโ€™t depend on anything external to the component, what makes it very scalable and easy to maintain.

Whenever a route is removed from the backstack, all of its own resources are destroyed. In app rotation, for example, the resources will remain on the current screen. Thanks to the rememberNavBackStack, we can also use a simple mutable list in the ViewModel to handle it, but it is really optional. According to the official documentation, either approach is valid.

3- Replace Activities flow with Compose flows using Nav3​


Now, Iโ€™ll show the ChapletRouteComponent which I already used as an example of migrating from fragment navigation to nav3 in my previous article. Back then, I using activity there, so currently we can see the transformation using only compose and nav3:


It looks almost the same as the ChapletActivityFlow. If you do not remember, here is the gist from the previous article:


The main difference is that there are no activities anymore, as it is 100% compose component, which controls its own backstack. In this case, the backstack is in the ViewModel. You can ask: why are you using rememberNavBackStack in some places and ViewModel in others? Just because I wanted to demonstrate that both approaches are valid :D.


ChapletFlowViewModel is just to manage the backstack of this flow and update the challenges once the user is done with chaplet.

Conclusion and point of attention:​


As youโ€™ve seen, this approach follows the single responsibly pattern, where each route is responsible for managing its own navigation stack. This makes the architecture much easier to maintain, as we donโ€™t need to handle crazy navigation stuff, because each route manages its own backstack.

It also survives configuration changes, whether youโ€™re using backstack in the ViewModel or rememberNavBackStack.

When a route is removed from the backstack, all of its resources are destroyed, preventing memory leaks.

And itโ€™s also important to revisit the role of entryDecorators, which I will copy from my previous article:

entryDecorators:

  • rememberSceneSetupNavEntryDecorator: it intelligently manages screen content, allowing it to move without costly recompositions. This results in better performance and more reliable UI behavior during transitions.
  • rememberSavedStateNavEntryDecorator: It provides the necessary infrastructure within the โ€œNavigation 3โ€ framework so that rememberSaveable works as intended on a per-screen basis. It isolates the saved state logic for each screen, preventing conflicts and ensuring that user progress and UI state are reliably preserved and restored. This is a fundamental piece for building apps that handle system-initiated process death and configuration changes gracefully
  • rememberViewModelStoreNavEntryDecorator: To ensure that screen-specific data managers (ViewModels) are correctly tied to each screenโ€™s lifecycle, this function is essential. It enables tools like Koin (in our case) to provide fresh, correctly scoped ViewModels for every screen, preventing data conflicts and ensuring proper cleanup. This results in more robust and predictable screen behavior.

Letโ€™s take a look and compare how it is now and how it was before:

1*Vl4PJ0yiBavZ7efQVtRZCw.gif

Extra:​


Iโ€™m sharing the code of PrayersRouteComponent just to show how easy it is to โ€œPlugโ€ a route into any other component:


This is the PrayersRouteComponent and it can navigate to: PrayerDetailScreen, ChapletFlowRouteComponent and NovenaFlowRouteComponent.

Itโ€™s very easy. We can just call RouteComponents from other route components without any side effects.

If you have any questions, feel free to message me here or reach out on Linkedin.

stat



Nested routes with Navigation 3 was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

Continue reading...
 


Join ๐•‹๐•„๐•‹ on Telegram
Channel PREVIEW:
Back
Top