NavController — bản chất, phương thức và quản lý điều hướng trong Jetpack Compose

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

NavController là thành phần trung tâm của thư viện Navigation Compose, quản lý ngăn xếp điều hướng và trạng thái back stack trong các ứng dụng Android. Thông qua NavController, các chuyển tiếp giữa màn hình, quay lại trang trước và truyền dữ liệu giữa các tuyến đường được thực hiện. Theo Android Developers (2025), NavController là yếu tố bắt buộc của mọi ứng dụng Compose có nhiều hơn một màn hình. Bộ điều khiển được tạo qua rememberNavController(), truyền đến NavHost và có sẵn để gọi navigate() từ bất kỳ điểm nào trong thành phần. Hỗ trợ tích hợp SavedStateHandle tự động lưu trạng thái ViewModel trong quá trình cấu hình lại.

Những Điểm Chính

  • NavController — bộ điều khiển điều hướng trung tâm của Compose, quản lý back stack và chuyển tiếp giữa các màn hình
  • navigate() — phương thức chính để điều hướng đến một tuyến đường với hỗ trợ NavOptions để quản lý ngăn xếp
  • popBackStack() — quay lại màn hình trước với tùy chọn dọn dẹp đến một tuyến đường chỉ định
  • SavedStateHandle — tích hợp với ViewModel để bảo toàn trạng thái màn hình trong quá trình điều hướng
  • currentBackStackEntryAsState() — quan sát tuyến đường hiện tại để đồng bộ hóa giao diện người dùng

NavController trong Jetpack Compose là gì?

NavController là một lớp từ thư viện Navigation Compose triển khai bộ điều khiển điều hướng cho các ứng dụng Compose. NavController quản lý ngăn xếp NavBackStackEntry, nơi mỗi mục chứa tuyến đường, đối số và trạng thái màn hình. Bộ điều khiển hỗ trợ các thao tác điều hướng cơ bản: chuyển tiếp, quay lại, thay thế và dọn dẹp.

Không giống như hệ thống View, nơi điều hướng diễn ra qua FragmentManager hoặc Intent, NavController hoạt động độc quyền trong ngữ cảnh Compose. Back stack được lưu trữ dưới dạng đồ thị NavDestination thay vì ngăn xếp Fragment. Điều này loại bỏ chi phí tạo và hủy Fragment và đơn giản hóa việc kiểm thử — NavController có thể được mô phỏng qua TestNavHostController.

NavController được liên kết chặt chẽ với NavHost — một vùng chứa hiển thị màn hình hiện tại từ đồ thị. Nếu không có NavHost, NavController không thể hiển thị các hàm composable nhưng vẫn giữ khả năng quản lý ngăn xếp. Trong kiến trúc điển hình, NavController được tạo ở cấp Activity hoặc composable chính và được truyền xuống cây thành phần qua các tham số.

Theo Google, NavController đã trải qua nhiều bản phát hành lớn. Phiên bản 2.8.0 đã thêm Type-Safe Navigation, phiên bản 2.9.0 đã thêm hỗ trợ predictive back gesture (Android 14+). Bộ điều khiển tương thích với Material3 Scaffold và BottomNavigation. Đối với các dự án đa mô-đun, NavController được truyền qua DI (Hilt/Koin) hoặc tham số hàm tạo.

NavController được tạo qua hàm composable rememberNavController(). Hàm trả về một thể hiện của NavHostController (lớp con của NavController) gắn với vòng đời của composable hiện tại. Khi rời khỏi thành phần, bộ điều khiển được dọn dẹp. Để bảo toàn bộ điều khiển trong quá trình cấu hình lại, hãy sử dụng rememberSaveable hoặc ViewModel.

kotlin
@Composable
fun MyApp() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = "main"
    ) {
        composable("main") { MainScreen(navController) }
        composable("details") { DetailsScreen(navController) }
    }
}

Cấu hình NavController bao gồm: NavHostController (chính), TestNavHostController (kiểm thử) và ScopedNavController (con cho đồ thị lồng nhau). Đối với BottomNavigation, NavController phải là duy nhất cho toàn bộ ứng dụng — tạo bộ điều khiển mới trong mỗi tab sẽ dẫn đến mất ngăn xếp. Để truyền bộ điều khiển đến các màn hình lồng nhau, hãy sử dụng tham số hàm thay vì CompositionLocalProvider để duy trì tính dễ đọc.

Để kiểm thử điều hướng, hãy sử dụng TestNavHostController với compose-test-rule. Bộ điều khiển cho phép đặt tuyến đường ban đầu và xác minh rằng navigate() đã kích hoạt chuyển tiếp mong đợi. Kiểm thử NavController không yêu cầu trình giả lập — nó hoạt động với các bộ so khớp Semantics của Compose Test.

Phương thức navigate(route: String) là cơ chế điều hướng chính trong NavController. Nó chấp nhận một chuỗi tuyến đường, NavOptions tùy chọn và Navigator.Extras. NavOptions kiểm soát hành vi chuyển tiếp: launchSingleTop (không trùng lặp tuyến đường trong ngăn xếp), popUpTo (dọn dẹp ngăn xếp đến một tuyến đường), restoreState (khôi phục trạng thái trước đó).

NavOptions được đặt qua cú pháp builder: NavOptionsBuilder. Các tham số chính: popUpTo (tuyến đường + inclusive/saveState), launchSingleTop (Boolean, true — không tạo bản sao), restoreState (khôi phục trạng thái khi quay lại). Nếu không có popUpTo, mỗi navigate() thêm một mục vào ngăn xếp, dẫn đến tích tụ back stack và hành vi sai của nút Back.

kotlin
navController.navigate("profile/42") {
    popUpTo("main") { saveState = true }
    launchSingleTop = true
    restoreState = true
}

Navigator.Extras cho phép truyền dữ liệu bổ sung không thuộc tuyến đường: các phần tử chia sẻ cho hoạt ảnh, cờ Intent, gói Pac-Man. Extras hiếm khi được sử dụng — chủ yếu để tích hợp với Accompanist Animation hoặc Navigators tùy chỉnh. Đối với hầu hết các kịch bản, một chuỗi tuyến đường và NavOptions là đủ.

popBackStack: quản lý quay lại và dọn dẹp ngăn xếp

popBackStack() là phương thức để quay lại màn hình trước. Không có đối số, nó xóa mục trên cùng của ngăn xếp và trả về true nếu việc xóa thành công. Nếu ngăn xếp trống, phương thức trả về false và Activity đóng lại (tương tự super.onBackPressed()).

Phiên bản nạp chồng popBackStack(route: String, inclusive: Boolean) xóa tất cả các mục cho đến tuyến đường được chỉ định. Nếu inclusive = true, chính tuyến đường được chỉ định cũng bị xóa. Phương thức trả về Boolean — true nếu tìm thấy và xóa các mục. Phiên bản inclusive hữu ích cho các kịch bản “thoát đến màn hình gốc” sau khi ủy quyền hoặc hoàn tất đơn hàng.

Phương thứcMô tảVí dụ
popBackStack()Quay lại một màn hìnhnavController.popBackStack()
popBackStack(route, false)Dọn đến route (route giữ lại)popBackStack(“home”, false)
popBackStack(route, true)Dọn đến và bao gồm routepopBackStack(“home”, true)
navigate(route) { popUpTo(route) { inclusive = true } }Chuyển tiếp với dọn dẹp hoàn toànnavigate(“login”) { popUpTo(0) { inclusive = true } }

Để xử lý nút Back hệ thống (hardware back button), hãy sử dụng BackHandler từ Compose. BackHandler chấp nhận enabled và onBack — một callback được gọi khi nhấn. Đối với Android 14+, PredictiveBackGesture được sử dụng, tích hợp qua NavController từ phiên bản 2.9.0. Predictive back thêm hoạt ảnh xem trước quay lại.

SavedStateHandle: bảo toàn trạng thái màn hình

SavedStateHandle là một cơ chế bảo toàn trạng thái ViewModel trong quá trình điều hướng và cấu hình lại. NavController tự động cung cấp SavedStateHandle cho mỗi NavBackStackEntry. Qua SavedStateHandle, ViewModel lưu trữ trạng thái màn hình và khôi phục khi quay lại (restoreState = true).

Trong Navigation Compose, SavedStateHandle được sử dụng cùng với ViewModel: ViewModel được khởi tạo qua SavedStateHandle, được truyền từ backStackEntry. Khi điều hướng đến màn hình khác và quay lại (với restoreState), ViewModel nhận trạng thái đã lưu thay vì được tạo mới. Điều này rất quan trọng đối với các màn hình có nhập dữ liệu, bộ lọc hoặc cuộn.

kotlin
class ProfileViewModel(
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {
    val userId: String = savedStateHandle.get<String>("userId") ?: ""
    var searchQuery by savedStateHandle.getStateFlow("search", "")
        .collectAsState()
}

SavedStateHandle hỗ trợ các kiểu nguyên thủy, String, Bundle và Parcelable. Đối với các đối tượng phức tạp, chỉ lưu ID và tải dữ liệu đầy đủ từ kho lưu trữ. Giới hạn của SavedStateHandle là khoảng 1 MB, vượt quá sẽ gây ra TransactionTooLargeException. Đối với khối lượng lớn, hãy sử dụng Room hoặc DataStore thay vì lưu trong handle.

Quan trọng: SavedStateHandle chỉ bảo toàn trạng thái khi restoreState = true được sử dụng trong NavOptions. Nếu restoreState không được chỉ định, ViewModel được tạo mới với các giá trị mặc định khi quay lại. Đối với chuyển đổi BottomNavigation với restoreState, NavController bảo toàn trạng thái của mỗi tab và khôi phục khi chọn lại.

Quan sát tuyến đường hiện tại qua currentBackStackEntryAsState

currentBackStackEntryAsState() là một hàm trả về State<NavBackStackEntry?>, cập nhật mỗi khi tuyến đường hiện tại thay đổi. Đây là cơ chế chính để đồng bộ hóa giao diện người dùng với điều hướng: BottomNavigation làm nổi bật mục đang hoạt động, Toolbar cập nhật tiêu đề, Drawer đóng khi chuyển tiếp.

Hàm hoạt động qua snapshotFlow và collectAsState: khi back stack thay đổi, Compose tái tạo các phần tử đã đăng ký. Quan trọng: currentBackStackEntryAsState() chỉ cập nhật sau khi hoạt ảnh chuyển tiếp hoàn tất. Để cập nhật ngay lập tức, hãy sử dụng currentDestination, thay đổi đồng bộ với navigate() nhưng không hỗ trợ trạng thái.

kotlin
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route

Text(
    text = when (currentRoute) {
        "home" -> "Home"
        "profile" -> "Profile"
        else -> ""
    }
)

Để truy cập các đối số của tuyến đường hiện tại, hãy sử dụng navBackStackEntry?.arguments. Điều này thuận tiện trong BottomNavigation: selectedItem được tính dựa trên currentRoute. Để gỡ lỗi điều hướng, hãy sử dụng NavController.addOnDestinationChangedListener() ghi lại mỗi chuyển tiếp. Trong sản xuất, tránh đăng ký bên trong số lượng lớn composable — hãy tạo một nguồn duy nhất trong ViewModel và truyền State đến giao diện người dùng.

Câu Hỏi Thường Gặp

Có thể tạo nhiều NavController trong một Activity không?

Về mặt kỹ thuật là có, nhưng không được khuyến khích. Một NavController duy nhất đảm bảo back stack nhất quán và đơn giản hóa việc gỡ lỗi. Nhiều bộ điều khiển chỉ hợp lý cho các đồ thị lồng nhau với điều hướng riêng biệt (ví dụ: modal bottom sheet với ngăn xếp riêng).

Làm thế nào để truyền NavController qua ViewModel?

Truyền NavController đến ViewModel qua hàm tạo hoặc DI. Tuy nhiên, tốt hơn là chỉ truyền các hàm callback (onNavigate, onBack) thay vì chính NavController — điều này đơn giản hóa việc kiểm thử. Đối với sự kiện, hãy sử dụng Channel<NavEvent> trong ViewModel và thu thập trong giao diện người dùng.

Tại sao navigate không hoạt động sau một thao tác bất đồng bộ?

Vấn đề là vòng đời: nếu NavController chưa được khởi tạo (NavHost chưa được xây dựng), navigate() bị bỏ qua. Sử dụng LaunchedEffect để gọi điều hướng sau khi tải dữ liệu, không phải bên trong một coroutine có vòng đời tùy ý.

Làm thế nào để dọn sạch toàn bộ back stack và điều hướng đến màn hình mới?

Gọi navController.navigate(“target”) { popUpTo(0) { inclusive = true } }. Tham số popUpTo(0) dọn sạch ngăn xếp hoàn toàn, inclusive = true cũng xóa mục bắt đầu. Cờ launchSingleTop = true ngăn chặn các tuyến đường trùng lặp.

Sự khác biệt giữa NavHostController và NavController là gì?

NavHostController là một lớp con của NavController với các phương thức bổ sung cho NavHost (ví dụ: setOnBackStackChangedListener). NavController là lớp cơ sở có thể được sử dụng bên ngoài NavHost để quản lý ngăn xếp theo chương trình. Trong hầu hết các trường hợp, NavHostController được sử dụng.

Tổng Kết

  • NavController — thành phần trung tâm của Navigation Compose, quản lý ngăn xếp tuyến đường và chuyển tiếp giữa các màn hình
  • navigate() thực hiện chuyển tiếp với cài đặt popUpTo, launchSingleTop và restoreState qua NavOptions
  • popBackStack() quản lý quay lại: bước đơn hoặc dọn dẹp hàng loạt đến một tuyến đường chỉ định với inclusive
  • SavedStateHandle tích hợp với ViewModel để tự động bảo toàn trạng thái màn hình trong quá trình điều hướng
  • currentBackStackEntryAsState() cung cấp quan sát phản ứng của tuyến đường hiện tại để đồng bộ hóa giao diện người dùng
  • BackHandler xử lý nút Back hệ thống, PredictiveBackGesture được hỗ trợ từ NavController 2.9.0
  • Để kiểm thử, hãy sử dụng TestNavHostController với compose-test-rule và bộ so khớp Semantics

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