BottomNavigation: cấu trúc và thiết lập trong Jetpack Compose

Tác giả: IT Sectr Đã đăng: 2026-06-29 Thời gian đọc: 8 phút

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 — thanh điều hướng dưới cùng cho 3–5 màn hình, triển khai qua NavigationBar trong Material3
  • NavigationBarItem — mục bảng điều khiển chứa biểu tượng, nhãn và huy hiệu cho thông báo
  • Kết nối với NavHost — chọn một mục sẽ kích hoạt navigate() trong NavController để chuyển đồ thị
  • Material3 thêm Màu Động và xử lý tự động điều hướng hệ thống qua WindowInsets
  • Badge — huy hiệu thông báo trên biểu tượng, triển khai qua BadgedBox với số đếm

BottomNavigation trong Jetpack Compose là gì?

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.

kotlin
@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ạiMô tả
icon@Composable () -> UnitBiể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
selectedBooleanCờ mục đã chọn
onClick() -> UnitCallback khi nhấp
badge@Composable (() -> Unit)?Huy hiệu với số lượng thông báo

Kết nối BottomNavigation với NavHost

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.

kotlin
@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.

NavigationBar trong Material3: Màu Động và thích ứng

Material3 (M3) cung cấp NavigationBarNavigationBarItem 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.

Tùy chỉnh màu sắc NavigationBar

Để 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.

Huy hiệu thông báo và bộ đếm trên các mục

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).

kotlin
@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

BottomNavigation nên có bao nhiêu mục?

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.

Làm thế nào để ẩn BottomNavigation trên màn hình con?

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.

Sự khác biệt giữa NavigationBar (M3) và BottomNavigation (M2) là gì?

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ó.

Tại sao màn hình bị tạo lại khi nhấp vào mục BottomNavigation?

Vấn đề là thiếu saveStaterestoreState 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.

Làm thế nào để tùy chỉnh hoạt ảnh chuyển đổi giữa các tab?

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

  • BottomNavigation (NavigationBar trong M3) — thanh dưới cùng để chuyển đổi giữa 3–5 màn hình chính của ứng dụng
  • NavigationBarItem — mục bảng điều khiển với biểu tượng, nhãn, huy hiệu và trạng thái chọn
  • Tích hợp với NavController yêu cầu popUpTo, launchSingleTop và restoreState để quản lý ngăn xếp chính xác
  • NavigationBar của Material3 hỗ trợ Màu Động, BadgedBox, WindowInsets và hoạt ảnh mục đang hoạt động
  • BadgedBox — thành phần cho huy hiệu thông báo với số hoặc dấu chấm trên biểu tượng mục
  • Trạng thái chọn được tính từ currentBackStackEntryAsState để đồng bộ hóa với NavController
  • Để ẩn thanh trên màn hình con, sử dụng kết xuất có điều kiện trong Scaffold bottomBar dựa trên danh sách route

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.

Thảo luận dự án

Đọc thêm