Compose Multiplatform in Action: Implementing a Bottom Navigation Bar in CMP

Introduction

Compose Multiplatform (CMP)

Today we’ll implement a NavigationBar bottom bar in CMP
It’s a composable component provided in Material 3
that allows users to create a bottom bar commonly used for page switching in apps

The finished implementation will look like this:

https://ithelp.ithome.com.tw/upload/images/20240812/201683355J8smYXCg7.png

Implementing a Bottom NavigationBar

Today, I’ll walk through how to implement a NavigationBar bottom bar in CMP step by step
We need to define the structure of the bottom bar
and then add styles and behaviors to it

@Composable
fun BottomNavigation(navController: NavController) {
    val screens = listOf(
        Triple("🏠", "Lessons", ElegantJapaneseScreen.Learning.name),
        Triple("あ", "Ad", ElegantJapaneseScreen.Ad.name),
        Triple("🔍", "Grammar", ElegantJapaneseScreen.Grammar.name),
        Triple("👤", "Settings", ElegantJapaneseScreen.Setting.name)
    )

    NavigationBar(
        modifier = Modifier.height(60.dp),
        containerColor = MaterialTheme.colorScheme.surface,
    ) {
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentDestination = navBackStackEntry?.destination

        screens.forEach { (icon, label, route) ->
            NavigationBarItem(
                icon = { Text(icon) },
                label = { if (label.isNotEmpty()) Text(label) },
                selected = currentDestination?.route == route,
                colors = NavigationBarItemDefaults.colors(
                    selectedIconColor = Color.Blue,
                    selectedTextColor = Color.Blue,
                    indicatorColor = Color.Blue.copy(alpha = 0.5f),
                    unselectedIconColor = Color.Gray,
                    unselectedTextColor = Color.Gray
                ),
                onClick = {
                    if (currentDestination?.route != route) {
                        navController.navigate(route) {
                            navController.graph.findStartDestination().route?.let {
                                popUpTo(it) {
                                    saveState = true
                                }
                            }
                            launchSingleTop = true
                            restoreState = true
                        }
                    }
                }
            )
        }
    }
}

Key code explanation:

  1. I defined a list screens: where Triple
    allows you to create a container with three parameters
    Through this custom content
    different NavigationBarItems are generated
  2. NavigationBar( modifier = Modifier.height(60.dp), containerColor = MaterialTheme.colorScheme.surface, ) {....}: This is like many other compose components
    using NavigationBar to wrap NavigationBarItem
    giving you a bottom bar with a height of 60 dp

  3. We expect to pass in navController: NavController:
    This is the controller we used in previous days for page navigation
    (refer back if you’ve forgotten)

  4. val navBackStackEntry by navController.currentBackStackEntryAsState():
    This line uses Kotlin’s by syntax to create a navBackStackEntry variable
    and delegates it to the return value of navController.currentBackStackEntryAsState()

  5. val currentDestination = navBackStackEntry?.destination:
    This line extracts the current destination from navBackStackEntry

  6. Steps 4~5 above are mainly to get the current navigation destination
    allowing us to handle UI state updates based on logic

  7. selected = currentDestination?.route == route:
    This line prevents issues when clicking the same BottomBarItem on the same screen

  8. The parameters for NavigationBarItem are similar to other composables
    and can be adjusted according to the developer’s scenario

Practical Usage

Remember how we wrapped our Compose Navigation in a Scaffold?
Now
we can directly add our newly implemented BottomNavigation
to the bottomBar of the Scaffold

@Composable
fun ElegantAccessApp(
    vm: MainViewModel,
    navController: NavHostController = rememberNavController(),
) {
    vm.navController = navController

    navController.addOnDestinationChangedListener { _, destination, _ ->
        println("Navigation: Navigated to ${destination.route}")
    }

    Scaffold(
        // Add it here
        bottomBar = {
           BottomNavigation(navController)
        },
    ) { paddingValues ->
        NavHost(
            navController = navController,
            startDestination = ElegantJapaneseScreen.Learning.name,
            modifier = Modifier
                .padding(paddingValues)
                .safeDrawingPadding()
                .fillMaxSize()
        ) {
            routeLearningScreen(navController)
            routeAdScreen(navController)
            routeAScreen(navController)
            routeBScreen(navController)
            routeCScreen(navController)
            routeSettingScreen(navController)
        }

    }
}
What if some pages shouldn't show the NavigationBar?

In that case
we can write a function shouldShowBottomBar
to determine whether to show the NavigationBar on the current screen

The method is simple

Create a list containing the routes where you want to show the NavigationBar
and compare with the current route

This is where our previously defined enum shows its advantage
Through the well-defined enum
we just need to find the corresponding page Route and add it to the list

@Composable
fun shouldShowBottomBar(navController: NavHostController): Boolean {
    val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
    return currentRoute in listOf(
        ElegantJapaneseScreen.Learning.name,
        ElegantJapaneseScreen.Ad.name,
        ElegantJapaneseScreen.Contest.name,
        ElegantJapaneseScreen.Grammar.name,
        ElegantJapaneseScreen.About.name,
        ElegantJapaneseScreen.Setting.name
    )
}

Then add an if statement in the Scaffold

Scaffold(
    bottomBar = {
        if (shouldShowBottomBar(navController)) {
            BottomNavigation(navController)
        }
    },
)

You might also enjoy