Compose Multiplatform 實戰:CMP中實作NavigationBar底部欄
Compose Multiplatform (簡稱CMP)
今天我們要來實作 CMP
的NavigationBar底部欄
他也是在material 3 中有提供的一個composable元件
可以提供使用者製作App中常使用切換頁面的底部欄
實際做出來會看起來像這樣
目錄
- 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開發跨平台資料庫 & 疑難雜症
今天我將分步介紹如何在 CMP
中實作一個 NavigationBar 底部欄
我們需要定義底部欄的結構
然後為其添加樣式
和行為
@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
}
}
}
)
}
}
}
關鍵程式碼解說
:
- 我定義了一個list
screens
:其中的Triple
可以讓你放入三參數的一個容器
透過這邊自定義的內容
去產生不同的NavigationBarItem
-
NavigationBar( modifier = Modifier.height(60.dp), containerColor = MaterialTheme.colorScheme.surface, ) {....}
: 這邊一樣是我們眾多compose的起手式
用NavigationBar去包NavigationBarItem
這樣你就可以得到一個高度是60 dp的底部欄 -
我們預期會傳入
navController: NavController
:
這個是前幾天我們用來導航頁面的控制器
(忘記的可以回去看) -
val navBackStackEntry by navController.currentBackStackEntryAsState()
:
這行代碼使用了 Kotlin 的by
語法來創建一個navBackStackEntry
變量
並將其委託給 navController.currentBackStackEntryAsState() 的返回值 -
val currentDestination = navBackStackEntry?.destination
:
這行從navBackStackEntry
中提取當前目的地 (currentDestination) -
而以上
4~5
,主要是為了拿到當前目的地的導航
讓我們可以在這邊根據邏輯去處理UI狀態的更新 -
selected = currentDestination?.route == route
:
加入這行主要是為了防止在同個畫面又點擊了同個BottomBarItem的問題 - NavigationBarItem的參數就跟之前的其他comsable類似
可以根據開發者情境去調整內容
還記得我們前面的Compose Navigation
外面包了一層Scaffold
嗎?
這時候
我們就可以直接在Scaffold
中的bottomBar
中
加入我們剛實作好的BottomNavigation
了
@Composable
fun ElegantAccessApp(
vm: MainViewModel,
navController: NavHostController = rememberNavController(),
) {
vm.navController = navController
navController.addOnDestinationChangedListener { _, destination, _ ->
println("Navigation: Navigated to ${destination.route}")
}
Scaffold(
// 加在這裡
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)
}
}
}
那這時候
就可以寫一個function shouldShowBottomBar
用來判斷當前是否要顯示NavigationBar
方法也很簡單
建立一個list存放想要顯示NavigationBar的route
跟當前route
比較
這時候前面定義的enum就發揮優勢了
透過定義好的enum
這邊只需要去找到對應頁面的Route放進就好
@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
)
}
接著在Scaffold
加入if判斷
即可
Scaffold(
bottomBar = {
if (shouldShowBottomBar(navController)) {
BottomNavigation(navController)
}
},
)