BottomNavigation (NavigationBar trong Material3) là một thành phần Material Design cung cấp điều hướng giữa ba đến năm màn hình chính của ứng dụng. Trong Jetpack Compose, thanh dưới cùng được triển khai qua NavigationBar hoặc BottomNavigation, trong đó mỗi mục được biểu diễn bằng một biểu tượng và nhãn văn bản. Theo Google Material Design (2025), BottomNavigation được khuyến nghị cho iOS và Android như một mẫu điều hướng chính. Bảng điều khiển tự động điều chỉnh theo chiều cao của hệ thống điều hướng (Gesture Navigation) thông qua WindowInsets. NavigationBar trong Material3 hỗ trợ màu động, huy hiệu và lề thích ứng.
Những điểm chính
BottomNavigation là một thành phần Material Design cho thanh điều hướng dưới cùng hiển thị ba đến năm mục menu. Trong Jetpack Compose, nó được triển khai qua các hàm composable BottomNavigation (Material2) và NavigationBar (Material3). Thanh được đặt ở cuối màn hình và cung cấp truy cập nhanh đến các phần chính của ứng dụng.
Mục đích chính của BottomNavigation là tổ chức điều hướng chính giữa các điểm đến cấp màn hình. Mỗi mục bảng điều khiển đại diện cho một màn hình riêng biệt (Trang chủ, Tìm kiếm, Hồ sơ). Chạm vào sẽ chuyển đổi nội dung trong khu vực chính của màn hình. BottomNavigation không dành cho điều hướng lồng nhau — để làm điều đó, các tab ở trên cùng được sử dụng.
Theo Nguyên tắc Thiết kế Material Design (2025), thanh dưới cùng chỉ nên chứa các phần có thể truy cập từ bất kỳ điểm nào trong ứng dụng. Ẩn thanh trên các màn hình con (ví dụ: khi mở trang chi tiết) là một thực hành tiêu chuẩn. Trong Compose, khả năng hiển thị của BottomNavigation được kiểm soát qua trạng thái Scaffold hoặc kết xuất có điều kiện dựa trên route hiện tại của NavController.
Trong Material3, nên sử dụng NavigationBar thay vì BottomNavigation cũ. NavigationBar bao gồm Màu Động, hỗ trợ WindowInsets và cải thiện hoạt ảnh nhấn. Theo Google, NavigationBar là thành phần hướng tới tương lai, trong khi BottomNavigation từ M2 chỉ còn lại để tương thích ngược.
Mỗi mục NavigationBar được triển khai qua composable NavigationBarItem. Tham số: icon (biểu tượng), label (nhãn văn bản), selected (trạng thái chọn) và onClick (callback). NavigationBarItem tự động áp dụng màu đã chọn (primary) và tạo hoạt ảnh thay đổi kích thước biểu tượng khi kích hoạt.
@Composable
fun BottomNavBar() {
val items = listOf(
Screen.Home,
Screen.Search,
Screen.Profile
)
var selectedItem by remember { mutableIntStateOf(0) }
NavigationBar {
items.forEachIndexed { index, item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = selectedItem == index,
onClick = { selectedItem = index }
)
}
}
}
Biểu tượng cho NavigationBarItem nên là Đầy (Filled) cho trạng thái đã chọn và Viền (Outlined) cho trạng thái chưa chọn. Material Icons cung cấp cả hai bộ: Icons.Filled.Home và Icons.Outlined.Home. Theo mặc định, NavigationBarItem hiển thị biến thể đầy cho trạng thái đã chọn. Nếu cần hiển thị khác, hãy truyền biểu tượng có điều kiện: if (selected) Filled.X else Outlined.X.
Độ dài nhãn văn bản không được vượt quá 12 ký tự (Latin) hoặc 8 ký tự (Cyrillic). Nhãn dài bị cắt với dấu ba chấm. Cho nhãn, sử dụng maxLines = 1 và ellipsis = TextTruncation.Ellipsis. Nếu nhãn phải luôn hiển thị, hãy xóa tham số alwaysShowLabel = true (mặc định là true).
| Tham số | Loại | Mô tả |
|---|---|---|
| icon | @Composable () -> Unit | Biểu tượng của mục bảng điều khiển |
| label | @Composable (() -> Unit)? | Nhãn văn bản dưới biểu tượng |
| selected | Boolean | Cờ mục đã chọn |
| onClick | () -> Unit | Callback khi nhấp |
| badge | @Composable (() -> Unit)? | Huy hiệu với số lượng thông báo |
Cập nhật trực tiếp selectedItem không kích hoạt điều hướng — để chuyển đổi màn hình cần tích hợp với NavController. Mẫu điển hình: onClick trong NavigationBarItem gọi navController.navigate(route), và selectedItem được xác định từ route hiện tại của NavController.
NavController lưu trữ ngăn xếp quay lại hiện tại. Để xác định mục bảng điều khiển nào được chọn, so sánh route hiện tại (navController.currentBackStackEntryAsState().value?.destination?.route) với các route của mục. Nếu route khớp, mục đó được coi là đã chọn. Điều này loại bỏ trùng lặp trạng thái.
@Composable
fun MainScreen(navController: NavController) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
Scaffold(
bottomBar = {
NavigationBar {
items.forEach { item ->
NavigationBarItem(
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
selected = currentRoute == item.route,
onClick = {
navController.navigate(item.route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
}
) { /* content */ }
}
Cài đặt chính trong navigate(): popUpTo(startDestination) ngăn tích tụ ngăn xếp quay lại khi chuyển tab, launchSingleTop ngăn trùng lặp route trong ngăn xếp, và restoreState khôi phục trạng thái của tab đã truy cập trước đó. Nếu không có các tham số này, mỗi lần nhấp tạo một mục nhập mới trong ngăn xếp, dẫn đến hành vi nút Quay lại không chính xác.
Material3 (M3) cung cấp NavigationBar và NavigationBarItem thay thế cho BottomNavigation từ Material2. Ưu điểm chính của M3: Màu Động, chiều cao thích ứng, hoạt ảnh gợn sóng toàn chiều rộng và hỗ trợ WindowInsets cho điều hướng hệ thống.
Màu Động tự động chọn màu NavigationBar dựa trên hình nền thiết bị (Android 12+). Mục đang hoạt động được tô màu primary, mục không hoạt động được tô màu onSurfaceVariant. Theo mặc định, NavigationBar nâng lên trên điều hướng hệ thống qua Modifier.navigationBarsPadding(). Để điều khiển thủ công, sử dụng tham số windowInsets.
NavigationBar trong M3 tự động tạo hoạt ảnh chiều rộng của mục đang hoạt động — biểu tượng di chuyển sang trái và nhãn mở rộng. Hoạt ảnh kéo dài 300 ms và sử dụng easing tiêu chuẩn của Compose. Nếu hành vi này không mong muốn, hãy luôn hiển thị nhãn qua alwaysShowLabel = true trong NavigationBarItem.
Để ghi đè màu sắc, hãy truyền NavigationBarColors trong tham số colors. Theo mặc định, NavigationBarDefaults.colors() được sử dụng, nhưng có thể đặt giá trị tùy chỉnh: containerColor (nền thanh), contentColor (màu biểu tượng), indicatorColor (nền mục đang hoạt động). Không giống M2 nơi màu sắc được đặt qua BottomNavigationDefaults, M3 sử dụng giao diện NavColors thống nhất.
Nếu dự án sử dụng BottomNavigation từ M2 (androidx.compose.material), Google khuyến nghị di chuyển sang NavigationBar từ M3. Việc di chuyển bao gồm thay thế import, cập nhật tham số và thêm @OptIn(ExperimentalMaterial3Api::class). BottomNavigation từ M2 được coi là lỗi thời và không nhận được tính năng mới.
Trạng thái NavigationBar lưu trữ chỉ mục của mục hiện đang được chọn. Trong các trường hợp đơn giản, remember { mutableIntStateOf(0) } được sử dụng. Tuy nhiên, đối với ứng dụng sản xuất, chỉ mục phải được đồng bộ hóa với đồ thị điều hướng, không phải là trạng thái UI cục bộ.
Các cách tiếp cận chính để quản lý trạng thái: Dựa trên NavController (khuyến nghị) và Dựa trên ViewModel. Trong trường hợp đầu, selectedItem được tính từ route hiện tại của NavController. Trong trường hợp thứ hai, ViewModel lưu trữ màn hình hiện tại và NavigationBarItem đọc StateFlow từ ViewModel. Cách dựa trên NavController đơn giản hơn; cách dựa trên ViewModel cung cấp nhiều kiểm soát hơn cho logic phức tạp (xác thực, thử nghiệm A/B).
Khi sử dụng saveState và restoreState, NavController tự động lưu và khôi phục trạng thái của LazyColumn và các thành phần khác trong mỗi tab. Điều này loại bỏ nhu cầu về ViewModel để lưu trữ vị trí cuộn. Tuy nhiên, việc lưu không áp dụng cho các trường nhập — chúng yêu cầu SavedStateHandle hoặc ViewModel.
Badge là một chỉ báo thông báo nhỏ gọn hiển thị trên biểu tượng NavigationBarItem. Trong Material3, huy hiệu được triển khai qua BadgedBox, bao bọc biểu tượng và đặt huy hiệu ở góc trên bên phải. Huy hiệu có thể chứa số (số lượng thông báo) hoặc dấu chấm (chỉ báo đơn giản về sự tồn tại).
@Composable
fun HomeItem(unreadCount: Int) {
NavigationBarItem(
icon = {
BadgedBox(badge = {
if (unreadCount > 0) {
Badge { Text(unreadCount.toString()) }
}
}) {
Icon(Icons.Filled.Home, contentDescription = "Trang chủ")
}
},
label = { Text("Home") },
selected = false,
onClick = { }
)
}
BadgedBox tự động định vị Badge ở góc trên bên phải của phần tử con. Nếu số lượng vượt quá 99, Badge hiển thị "99+". Để ẩn huy hiệu khi giá trị bằng 0, sử dụng if (count > 0). Badge từ Material3 mặc định có màu error (đỏ) với văn bản trắng. Tùy chỉnh qua BadgeDefaults.colors(containerColor, contentColor).
Trong Material2, huy hiệu không tồn tại — phải vẽ thủ công qua Canvas hoặc sử dụng thư viện bên thứ ba. Material3 đã giải quyết vấn đề này bằng một thành phần gốc. Theo Android Developers (2025), BadgedBox được sử dụng trong 40% ứng dụng có NavigationBar, trong đó 60% hiển thị tin nhắn chưa đọc.
Câu hỏi thường gặp
Material Design khuyến nghị 3 đến 5 mục. Ít hơn ba là không thực tế (hãy sử dụng Tab). Nhiều hơn năm làm giảm khả năng đọc của nhãn và kích thước mục tiêu chạm. Nếu cần hơn 5 phần, hãy sử dụng Navigation Rail (máy tính bảng) hoặc Drawer.
Xác định danh sách các route nơi thanh hiển thị (thường là route gốc). Trong Scaffold bottomBar, chỉ truyền NavigationBar nếu currentRoute nằm trong bottomNavRoutes. Một cách khác là NavHost với đồ thị riêng cho màn hình lồng nhau không có bottomBar.
NavigationBar từ Material3 hỗ trợ Màu Động, BadgedBox, WindowInsets và chiều rộng mục đang hoạt động có hoạt ảnh. BottomNavigation từ Material2 là API cũ không có các tính năng này. Google khuyến nghị sử dụng NavigationBar cho dự án mới và di chuyển các dự án hiện có.
Vấn đề là thiếu saveState và restoreState trong navigate(). Không có chúng, NavController không lưu trạng thái của tab trước đó. Thêm popUpTo(startDestination) { saveState = true } và restoreState = true trong NavOptions để bảo toàn LazyColumn, vị trí cuộn và trường nhập.
Hoạt ảnh chuyển đổi được kiểm soát ở cấp NavHost, không phải BottomNavigation. Thêm composable(route, enterTransition, exitTransition) với hoạt ảnh tùy chỉnh. Đối với BottomNavigation, hoạt ảnh mờ dần (fadeIn + fadeOut) với thời lượng 300 ms là điển hình.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm