Rewriting the BackStack. A Compose & Navigation story.(Bonus at the end)

TL;DR : Achieve your ideal back stack by sequentially calling popBackStack() and navigate() on your navigation controller.

Hugo Djemaa
3 min readNov 23, 2023

It all began while I was developing an Android application called CropNGrid, designed to craft grid posts for Instagram.

This is what a grid post looks like

I designed the application with three top-level destinations: Home, Info, and Grid List. Clicking on an image in the GridListScreen will lead you to the GridScreen. From the HomeScreen, you can select a picture you want to split and enter the CropperScreen. After completing the cropping process, clicking a button takes you to the GridScreen, which displays the grid resulting from the split and crop. The GridScreen is a direct child of the GridListScreen destination, so ideally, navigating back should land you in the GridListScreen.

😮‍💨🤯

A picture is worth a thousand words.

Maybe this one just worth a 500 words 😅

What is the problem ?

The issue here lies in the navigation flow. If you simply use the navigate() method from the CropperScreen to the GridScreen, pressing the back arrow or performing the back gesture will take the user back to the CropperScreen. If you send an email and you press back, you don’t want to come back to writing an email, right ?

💡

For this, there is a (well documented) solution : the NavOptions’ popUpTo().

fun NavController.navigateToGrid(gridId: String) {
this.navigate("grid/$gridId") { // this: NavOptionsBuilder
popUpTo(homeNavigationRoute) // Will clear the backstack and let only HomeScreen in it
launchSingleTop = true
}
}

Great! Now, what happens if I press the back button while on the GridScreen? I’m taken back to the Home. However, remember that I intended for a back action from the GridScreen to take me to the GridListScreen, its direct ancestor in the natural navigation flow.

Any ideas?

This question is not rhetorical; I extensively searched for online article about how to modify the back stack without success. I tried different methods (from okay-ish to very ugly), but finally, I found a solution that I’m somewhat happy with, and I wanted to share it with you. If you have an alternative approach, please share it in the comments!

NavHost(
navController = navController,
...
) { // this : NavGraphBuilder
...
cropperScreen(
onCropComplete = {
navController.popBackStack(homeNavigationRoute, false)
navController.navigateToGridList()
navController.navigateToGrid(it)
},
)

When the user completes the crop/split of the image in the CropperScreen, the app will invoke onCropComplete(). This function will sequentially: clear the back stack, navigate to the GridListScreen, and then navigate to the GridScreen.

Voilà!

The user smoothly arrives at the GridScreen with a neat back stack: GridListScreen on top and HomeScreen right after. All of this happens seamlessly without any blinking or lagging.

🥳🥳🥳

Bonus part :

If you are using transitions in the navigation component, you likely have a lot of boilerplate code. I created a class aimed at making NavHost transitions more readable.

Here’s how I set up the NavHost component:

    val transitions = Transitions.BASE
.addTransition(
homeNavigationRoute,
gridListNavigationRoute,
rightToLeftTransition(EaseOutCubic, 500)
).addTransition(
gridListNavigationRoute,
homeNavigationRoute,
leftToRightTransition(EaseOutCubic, 500)
).addTransition(
gridListNavigationRoute,
infoNavigationRoute,
leftToRightTransition(EaseOutCubic, 500)
).addTransition(
infoNavigationRoute,
gridListNavigationRoute,
rightToLeftTransition(EaseOutCubic, 500)
).addTransition(
homeNavigationRoute,
infoNavigationRoute,
rightToLeftTransition(EaseOutCubic, 500)
).addTransition(
infoNavigationRoute,
homeNavigationRoute,
leftToRightTransition(EaseOutCubic, 500)
)


NavHost(
navController = navController,
startDestination = startDestination,
enterTransition = transitions.enterTransitionChain,
exitTransition = transitions.exitTransitionChain,
popEnterTransition = transitions.popEnterTransitionChain,
popExitTransition = transitions.popExitTransitionChain,
modifier = modifier
)

If you’re interested in the implementation details, you can check out the Transitions class in the CropNGrid repository. The app is open source!

Don’t forget to clap if you enjoyed the article, and feel free to leave comments for suggestions and feedback!

--

--