Android Compose UI Bottom Navigation Bar 與 Navigation Drawer
大多數 App 都擁有全域的導航結構,如底部的 Bottom Navigation Bar 或側邊的 Navigation Drawer。我們要學習如何將它們與 NavController 結合。
資料結構定義
首先,定義一個 Sealed Class 來管理底部導航的選項。
sealed class Screen(val route: String, val title: String, val icon: ImageVector) {
object Home : Screen("home", "首頁", Icons.Filled.Home)
object Search : Screen("search", "搜尋", Icons.Filled.Search)
object Profile : Screen("profile", "個人", Icons.Filled.Person)
}
NavigationBar (底部導航列)
我們需要在 Scaffold 的 bottomBar 中監聽當前的 Route,以決定哪個 tab 該被高亮 (Active)。
val navController = rememberNavController()
// 取得目前的 BackStackEntry (用於判斷當前頁面)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
val items = listOf(Screen.Home, Screen.Search, Screen.Profile)
items.forEach { screen ->
NavigationBarItem(
icon = { Icon(screen.icon, contentDescription = null) },
label = { Text(screen.title) },
selected = currentRoute == screen.route, // 判斷是否選中
onClick = {
navController.navigate(screen.route) {
// 避免堆疊過多頁面:點擊 tab 時會清空 back stack 直到 start destination
popUpTo(navController.graph.findStartDestination().id) {
saveState = true // 保存狀態 (例如捲動位置)
}
launchSingleTop = true // 避免重複開啟同個頁面
restoreState = true // 恢復狀態
}
}
)
}
}
}
) { innerPadding ->
NavHost(
navController = navController,
startDestination = Screen.Home.route,
modifier = Modifier.padding(innerPadding) // 重要!避免被底部欄遮住
) {
composable(Screen.Home.route) { HomeScreen() }
composable(Screen.Search.route) { SearchScreen() }
composable(Screen.Profile.route) { ProfileScreen() }
}
}
ModalNavigationDrawer (側邊選單)
如果你的導航項目很多,適合使用 Drawer。
// 這是選單的「狀態」。它記錄了目前選單是 Closed(關閉)還是 Open(開啟)
// remember 確保當畫面重組時,這個狀態不會被重置
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
// 開啟或關閉選單是一個 suspend function,必須在 Coroutine 中執行
// 這個 scope 讓你在點擊按鈕時,能啟動一個 Coroutine 來執行開關動作
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
// 選單內容:這裡定義了側邊攔滑出來後長什麼樣子
drawerContent = {
ModalDrawerSheet {
Text("Drawer Header", modifier = Modifier.padding(16.dp))
Divider()
NavigationDrawerItem(
label = { Text("Settings") },
selected = false,
onClick = { /* ... */ }
)
}
}
) {
// 這邊是 App 主畫面內容
Scaffold(
topBar = {
TopAppBar(
title = { Text("App") },
navigationIcon = {
IconButton(onClick = {
// 啟動協程來開啟 Drawer 選單
scope.launch { drawerState.open() }
}) {
Icon(Icons.Filled.Menu, contentDescription = null)
}
}
)
}
) {
// App Content
}
}
小結
- 使用
currentBackStackEntryAsState來監聽路由變化,更新 UI 高亮狀態。 - popUpTo / saveState / restoreState 這三個參數是底部導航的黃金三角,確保了切換 tab 時狀態不流失且不會無限堆疊。