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:
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
:
-
I defined a list
screens
: whereTriple
allows you to create a container with three parameters
Through this custom content
different
NavigationBarItem
s are generated -
NavigationBar( modifier = Modifier.height(60.dp), containerColor = MaterialTheme.colorScheme.surface, ) {....}
: This is like many other compose componentsusing NavigationBar to wrap
NavigationBarItem
giving you a bottom bar with a height of 60 dp
-
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)
-
val navBackStackEntry by navController.currentBackStackEntryAsState()
:This line uses Kotlin's
by
syntax to create anavBackStackEntry
variableand delegates it to the return value of navController.currentBackStackEntryAsState()
-
val currentDestination = navBackStackEntry?.destination
:This line extracts the current destination from
navBackStackEntry
-
Steps
4~5
above are mainly to get the current navigation destinationallowing us to handle UI state updates based on logic
-
selected = currentDestination?.route == route
:This line prevents issues when clicking the same BottomBarItem on the same screen
-
The parameters for NavigationBarItem are similar to other composables
and can be adjusted according to the developer's scenario
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)
}
}
}
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)
}
},
)