Forem

RockAndNull
RockAndNull

Posted on • Originally published at paleblueapps.com on

Flat approach for tabbed Navigation in Jetpack Compose

Flat approach for tabbed Navigation in Jetpack Compose

When building apps with tabbed navigation in Jetpack Compose, it's common to face the challenge of mixing screens with and without bottom tabs. Traditionally, we'd solve this by creating two separate navigation graphs - one for the outer navigation (without tabs) and another for the inner navigation (within tabs). However, this approach can lead to several issues, particularly when working with Compose Web, where URL mapping doesn't properly reflect the internal navigation graph (hopefully we will have an upcoming post about Compose Web).

In this post, we'll explore a simpler, flatter approach that avoids these complications while maintaining clean navigation.

The traditional approach

The nested graphs approach worked but created several pain points. Nested navigation made deep linking complicated, URL mapping in Compose Web didn't reflect the internal tab structure, sharing data between the two navigation graphs required workarounds, and back navigation often behaved unpredictably.

The flatter approach

Instead of nested graphs, we now propose using a flat navigation structure with conditional tab visibility.

sealed class TabScreen(
    val route: Any,
    val title: StringResource,
    val icon: DrawableResource
) {
    data object Tab1 : TabScreen(...)
    data object Tab2 : TabScreen(...)
}

val tabs = listOf(
    TabScreen.Tab1,
    TabScreen.Tab2,
)

@Composable
fun MainComponent(navController: NavHostController = rememberNavController()) {
    ApplicationTheme {
        Surface(
            modifier = Modifier.fillMaxSize(),
            color = MaterialTheme.colorScheme.surface
        ) {
            Scaffold(
                bottomBar = {
                    BottomNavBar(navController)
                }
            ) { paddingValues ->
                Column(
                    modifier = Modifier.fillMaxSize()
                        .padding(paddingValues)
                        .consumeWindowInsets(paddingValues)
                ) {
                    MainNavigation(navController)
                }
            }
        }
    }
}

@Composable
private fun BottomNavBar(
    navController: NavHostController,
) {
    val navBackStackEntry by navController.currentBackStackEntryAsState()
    val currentDestination = navBackStackEntry?.destination

    val showBottomNav = tabs.any { tab ->
        currentDestination?.hierarchy?.any { it.hasRoute(tab.route::class) } == true
    }

    if (!showBottomNav) return

    Column {
        HorizontalDivider()
        NavigationBar(
            containerColor = Color.White
        ) {
            tabs.forEach { screen ->
                val isSelected = currentDestination?.hierarchy
                    ?.any { it.hasRoute(tab.route::class) } == true
                NavigationBarItem(
                    icon = {
                        Icon(
                            painter = painterResource(screen.icon),
                            contentDescription = null,
                            modifier = Modifier.size(24.dp)
                        )
                    },
                    label = { Text(stringResource(screen.title) },
                    selected = isSelected,
                    onClick = {
                        navController.navigate(screen.route) {
                            // Pop up to the start destination of 
                            // the graph to
                            // avoid building up a large stack 
                            // of destinations
                            // on the back stack as users select items
                            popUpTo(TabHostFlow) {
                                if (!isSelected) {
                                    saveState = true
                                }
                            }
                            // Avoid multiple copies of 
                            // the same destination when
                            // reselecting the same item
                            launchSingleTop = true
                            // Restore state when reselecting 
                            // a previously selected item
                            restoreState = true
                        }
                    },
                )
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This flatter approach offers several advantages. With a flat navigation structure, URLs in Compose Web properly reflect all destinations, making routing more predictable. The showBottomNav variable checks whether the current destination should display tabs - if not, the bottom navigation simply doesn't render. Individual tabs can be conditionally shown or hidden based on application state through parameters. Additionally, with a single navigation graph, back navigation behaves more intuitively and predictably, while deep linking becomes straightforward since all destinations exist at the same level in the navigation hierarchy.

The navigation host

Here's how the navigation host should be structured for the above code to work:

@Composable
fun MainNavigation(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = OnboardingFlow.Start,
    ) {
        // Non-tabbed flows
        authenticationGraph(navController = navController)
        onboardingGraph(navController = navController)

        // Wrap tab screens in a navigation block to preserve their state
        navigation<TabHostFlow>(startDestination = HomeFlow.Start) {
            homeGraph(navController = navController)
            myProfileGraph(navController = navController)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

The important detail that's crucial for proper state preservation when switching between tabs is wrapping the tab screen graphs in a navigation block. This ensures that the state of each tab is properly maintained when users switch between them. Without this wrapper, you might find that your tab states are reset when navigating between them, leading to a poor user experience.

Conclusion

By using a flat navigation structure with conditional tab visibility, we've simplified our navigation architecture while making it more flexible and web-friendly. This approach solves the URL mapping issues in Compose Web and provides a more intuitive user experience.

Happy coding!

Top comments (0)