NavHost là vùng chứa composable đóng vai trò là điểm vào cho đồ thị điều hướng trong Jetpack Compose. Nó liên kết NavController với một tập hợp các tuyến đường và hiển thị màn hình hiện tại dựa trên trạng thái của ngăn xếp quay lại. Theo Android Developers (2025), NavHost là thành phần bắt buộc cho bất kỳ ứng dụng Compose nào có điều hướng. Bên trong NavHost, các tuyến đường composable được đăng ký với các đối số tùy chọn, deep link và hoạt ảnh. Mỗi tuyến đường là một hàm composable thông thường nhận NavBackStackEntry với dữ liệu chuyển tiếp. NavHost tự động xử lý nút quay lại, lưu trạng thái và khôi phục khi cấu hình lại.
Những Điểm Chính
NavHost là một hàm composable cung cấp vùng chứa để hiển thị màn hình điều hướng hiện tại. NavHost nhận NavController, startDestination và đồ thị tuyến đường được xây dựng qua Kotlin DSL. Khi tuyến đường hiện tại thay đổi, NavHost chuyển đổi composable được hiển thị với hoạt ảnh đã chỉ định.
NavHost hoạt động như một bộ chuyển đổi màn hình: nó theo dõi NavBackStackEntry hiện tại từ NavController và hiển thị khối composable tương ứng. Mỗi màn hình là một hàm composable độc lập nhận NavBackStackEntry với các đối số tuyến đường. Tất cả các màn hình tồn tại trong một cây thành phần duy nhất, nhưng NavHost chỉ hiển thị một màn hình tại một thời điểm, ẩn các màn hình khác qua hoạt ảnh.
Không giống như FragmentManager, NavHost không tạo Fragment cho mỗi màn hình. Toàn bộ vòng đời được quản lý qua CompositionLifecycle — các hàm composable không có onStart/onResume, do đó LaunchedEffect và DisposableEffect được sử dụng cho các tác dụng phụ. NavHost tự động đăng ký NavController và kết hợp lại UI khi tuyến đường thay đổi.
Theo Google, NavHost là API ổn định từ Navigation 2.4.0. Bắt đầu từ 2.8.0, NavHost hỗ trợ điều hướng Type-Safe qua Kotlin Serialization, thay thế các tuyến đường chuỗi bằng các lớp dữ liệu. NavHost cũng hỗ trợ đồ thị lồng nhau, cho phép tổ chức điều hướng theo mô-đun.
NavHost được tạo với hai tham số bắt buộc: navController (một thể hiện của NavHostController) và startDestination (chuỗi tuyến đường của màn hình đầu tiên). Tham số thứ ba là khối xây dựng nơi tất cả các tuyến đường được đăng ký qua composable(), navigation() và dialog().
@Composable
fun AppNavHost(navController: NavHostController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") { HomeScreen(navController) }
composable("settings") { SettingsScreen(navController) }
}
}
startDestination là tuyến đường được mở khi NavHost khởi chạy lần đầu tiên. Nếu ngăn xếp quay lại trống, NavHost tự động thêm startDestination vào ngăn xếp. Khi cấu hình lại (xoay màn hình), NavHost khôi phục tuyến đường cuối cùng từ savedState, không phải startDestination.
Đối với BottomNavigation, startDestination là một trong các tuyến đường của bảng điều khiển phía dưới. Các tuyến đường bảng điều khiển còn lại được thêm dưới dạng các mục composable riêng biệt. NavHost nên được đặt bên trong Scaffold.content — nơi nội dung chính của ứng dụng được hiển thị. NavHost chiếm toàn bộ chiều cao khả dụng trừ TopAppBar và BottomNavigation.
Hàm composable(route, arguments, deepLinks, enterTransition, exitTransition, content) đăng ký một tuyến đường trong đồ thị NavHost. Tham số route là một chuỗi mô tả đường dẫn với các trình giữ chỗ tùy chọn dạng {paramName}. Trình giữ chỗ được thay thế bằng giá trị thực trong quá trình điều hướng.
Khối content của composable nhận NavBackStackEntry từ đó các đối số được trích xuất. Hàm composable của màn hình chỉ được hiển thị khi tuyến đường NavController hiện tại khớp với tuyến đường. Nếu không khớp, composable bị xóa khỏi thành phần, nhưng trạng thái của nó có thể được bảo toàn qua rememberSaveable hoặc ViewModel với SavedStateHandle.
composable(
route = "article/{articleId}",
arguments = listOf(navArgument("articleId") {
type = NavType.IntType
defaultValue = 0
}),
deepLinks = listOf(navDeepLink { uriPattern = "https://app.example/article/{articleId}" })
) { backStackEntry ->
val articleId = backStackEntry.arguments?.getInt("articleId") ?: 0
ArticleScreen(articleId = articleId)
}
Số lượng mục composable bên trong NavHost có thể từ vài đến hàng trăm. Đối với các ứng dụng lớn, các tuyến đường được chia thành các mô-đun và kết nối qua đồ thị lồng nhau. Mỗi composable có thể có cài đặt hoạt ảnh, deep link và đối số riêng.
Các đối số tuyến đường được định nghĩa qua tham số arguments: List<NamedNavArgument> trong composable(). Mỗi đối số được định nghĩa qua navArgument(name) { type; defaultValue }. NavType xác định loại đối số: StringType, IntType, LongType, FloatType, BoolType, ParcelableType và ReferenceType.
| Tham số tuyến đường | Ví dụ tuyến đường | NavType |
|---|---|---|
| Đường dẫn (path) | "user/{id}" | NavType.IntType |
| Truy vấn (query) | "search?q={query}" | NavType.StringType |
| Tùy chọn | "details/{id}?tab={tab}" | StringType + defaultValue="" |
| Parcelable | "checkout/{order}" | NavType.ParcelableType |
Các đối số được trích xuất từ NavBackStackEntry qua arguments?.getInt("id"). Đối với đối số bắt buộc, defaultValue có thể được bỏ qua — NavType sẽ sử dụng null. Đối với đối số tùy chọn, defaultValue phải được đặt, nếu không điều hướng sẽ ném ngoại lệ nếu tham số bị thiếu.
Từ Navigation 2.8.0, điều hướng Type-Safe được khuyến nghị: định nghĩa một sealed class hoặc data class cho các tuyến đường với Kotlin Serialization. Thay vì tuyến đường chuỗi, hãy sử dụng composable<RouteType> { backStackEntry -> }. Điều này loại bỏ lỗi chính tả trong tuyến đường và tự động tạo NavType cho các đối số. Để di chuyển, hãy thêm phụ thuộc navigation-compose-typesafe và plugin Kotlin Serialization.
nested graphs — cơ chế nhóm các tuyến đường bên trong NavHost bằng hàm navigation(route, startDestination). Đồ thị lồng nhau có tiền tố tuyến đường và startDestination riêng, và tất cả các tuyến đường của nó có thể truy cập qua tiền tố. Đồ thị lồng nhau được sử dụng cho kiến trúc mô-đun, nơi mỗi mô-đun tính năng đăng ký đồ thị con riêng của nó.
Lợi ích của đồ thị lồng nhau: cô lập tuyến đường trong mô-đun, ngăn xếp quay lại thống nhất cho một nhóm màn hình và khả năng điều hướng theo tiền tố mà không để lộ cấu trúc bên trong. Ví dụ, đồ thị "auth" chứa "auth/login" và "auth/register". Điều hướng có thể thực hiện qua tuyến đường đầy đủ hoặc qua tiền tố với chuyển hướng đến startDestination.
NavHost(navController = navController, startDestination = "main") {
composable("main") { MainScreen(navController) }
navigation(
route = "auth",
startDestination = "auth/login"
) {
composable("auth/login") { LoginScreen(navController) }
composable("auth/register") { RegisterScreen(navController) }
}
}
Đồ thị lồng nhau hỗ trợ truyền đối số ở cấp đồ thị: các tham số được khai báo trong tuyến đường của đồ thị được truyền đến tất cả các tuyến đường bên trong. Để xóa đồ thị lồng nhau, hãy sử dụng popBackStack(route) — nó sẽ xóa tất cả các mục bên trong. Đồ thị lồng nhau không có giới hạn độ sâu, nhưng không nên quá 3 cấp để dễ đọc.
NavHost hỗ trợ hoạt ảnh chuyển tiếp giữa các tuyến đường composable qua các tham số enterTransition, exitTransition, popEnterTransition và popExitTransition. Hoạt ảnh được đặt một lần cho NavHost và áp dụng cho tất cả các tuyến đường, hoặc riêng lẻ cho mỗi composable. Theo mặc định, hoạt ảnh bị tắt.
Cấu hình điển hình: enterTransition = slideInHorizontally(initialOffsetX = { it }) — màn hình trượt vào từ bên phải; exitTransition = slideOutHorizontally(targetOffsetX = { -it }) — màn hình trượt ra bên trái. Đối với hoạt ảnh pop, hướng được phản chiếu: màn hình trượt vào từ bên trái và trượt ra bên phải. Đối với BottomNavigation, fadeIn/fadeOut được sử dụng mà không trượt.
NavHost(
navController = navController,
startDestination = "home",
enterTransition = { slideInHorizontally(initialOffsetX = { it }) + fadeIn() },
exitTransition = { slideOutHorizontally(targetOffsetX = { -it }) + fadeOut() },
popEnterTransition = { slideInHorizontally(initialOffsetX = { -it }) + fadeIn() },
popExitTransition = { slideOutHorizontally(targetOffsetX = { it }) + fadeOut() }
) { /* composable routes */ }
Hoạt ảnh tùy chỉnh được tạo bằng Compose Animation API: AnimatedContentTransitionScope cung cấp quyền truy cập vào kích thước vùng chứa, tiến trình hoạt ảnh và hướng. Đối với chuyển tiếp phần tử dùng chung (một phần tử di chuyển mượt mà sang màn hình khác), cần thư viện Accompanist Navigation Animation hoặc triển khai tùy chỉnh qua sharedElement Modifier. Theo Android Developers (2025), hoạt ảnh trượt mặc định (vào từ bên phải, ra bên trái) được sử dụng trong 80% ứng dụng Android có điều hướng.
Câu Hỏi Thường Gặp
Về mặt kỹ thuật là có, nhưng không được khuyến nghị. Mỗi NavHost tạo một ngăn xếp quay lại độc lập, phá vỡ điều hướng thống nhất. Ngoại lệ là các khu vực riêng biệt, như NavHost cho nội dung chính và NavHost cho BottomSheet với điều hướng riêng của nó.
NavHost là vùng chứa điều hướng chuyển đổi màn hình. Scaffold là bố cục của toàn bộ trang (TopAppBar, BottomNavigation, FloatingActionButton). Thông thường, NavHost được đặt bên trong Scaffold.content. Scaffold không quản lý điều hướng, nó chỉ cung cấp các vị trí cho thành phần UI.
ViewModel được tạo trong NavBackStackEntry qua viewModel(). Để chia sẻ ViewModel giữa các màn hình, hãy sử dụng parentNavController: liên kết ViewModel dùng chung với mục cha. Một giải pháp thay thế là DI (Hilt/Koin) với phạm vi NavGraph.
Đây là hành vi bình thường — NavHost xóa composable khỏi thành phần khi rời khỏi tuyến đường. Để bảo toàn trạng thái, hãy sử dụng rememberSaveable cho trạng thái UI và ViewModel với SavedStateHandle cho logic nghiệp vụ.
Thêm tuyến đường cuối cùng composable("404") và điều hướng đến nó khi nhận được deep link không xác định. NavHost không có tuyến đường catch-all — hãy kiểm tra tuyến đường trong trình xử lý intent Deep Link trước navigate(). Nếu không tìm thấy tuyến đường, hãy điều hướng đến 404.
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