I
Iago Fucolo
Guest

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:

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.
material 3 reference: https://m3.material.io/components/navigation-bar/overview

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:

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

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:

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.
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...