Compose Multiplatform in Action: Implementing a Bottom Navigation Bar in CMP
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:
目錄
- Compose Multiplatform 實戰:放輕鬆點,初探CMP
- Compose Multiplatform 實戰:初戰,安裝CMP環境吧
- Compose Multiplatform 實戰:續戰,用Wizard創建CMP專案
- Compose Multiplatform 實戰:在Android、iOS模擬器上跑CMP專案
- Compose Multiplatform 實戰:CMP的專案結構理解與編譯配置
- Compose Multiplatform 實戰:CMP中跨平台Android、iOS程式碼的進入點
- Compose Multiplatform 實戰:在CMP的Compose中用Material Design3 Theme
- Compose Multiplatform 實戰:CMP用Compose實作跨平台畫面
- Compose Multiplatform 實戰:使用 expect 和 actual 實現跨平台程式碼
- Compose Multiplatform 實戰:CMP中實作Compose Navigation頁面切換
- Compose Multiplatform 實戰:CMP中透過StateFlow來管理UI狀態
- Compose Multiplatform 實戰:CMP中實作NavigationBar底部欄
- Compose Multiplatform 實戰:CMP中使用koin來依賴注入Dependency Injection
- Compose Multiplatform 實戰:CMP實作跨平台資料庫SqlDelight
- Compose Multiplatform 實戰:CMP中使用ROOM開發跨平台資料庫 & 疑難雜症
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
differentNavigationBarItem
s are generated -
NavigationBar( modifier = Modifier.height(60.dp), containerColor = MaterialTheme.colorScheme.surface, ) {....}
: This is like many other compose components
using NavigationBar to wrapNavigationBarItem
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’sby
syntax to create anavBackStackEntry
variable
and delegates it to the return value of navController.currentBackStackEntryAsState() -
val currentDestination = navBackStackEntry?.destination
:
This line extracts the current destination fromnavBackStackEntry
-
Steps
4~5
above are mainly to get the current navigation destination
allowing 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)
}
},
)