composable() là một hàm của thư viện Navigation Compose dùng để đăng ký màn hình trong NavHost và kết nối route URL với bố cục Compose. Khi điều hướng đến một route xác định, Jetpack Compose gọi hàm composable tương ứng và hiển thị nó như màn hình hiện tại. Không giống như FragmentManager hay điều hướng dựa trên Intent, composable() hoạt động ở cấp độ một Activity duy nhất và được quản lý hoàn toàn thông qua Kotlin DSL. Theo Android Developers (2025), hơn 73% ứng dụng Android hiện đại được xây dựng bằng Jetpack Compose sử dụng Navigation Compose để tổ chức chuyển đổi màn hình.
Những điểm chính
composable() là một hàm mở rộng của đối tượng NavHost. Kotlin DSL cho phép gọi nó bên trong khối NavHost để mô tả khai báo tất cả các màn hình của ứng dụng. Mỗi lần gọi tạo một mục nhập trong đồ thị điều hướng, liên kết một route chuỗi với một hàm composable. Khi người dùng điều hướng đến một route cụ thể, NavHost hiển thị composable tương ứng như màn hình hiện tại, ẩn màn hình trước đó.
Thư viện Navigation Compose được Google giới thiệu vào năm 2021 như một giải pháp thay thế cho điều hướng dựa trên Fragment cho Jetpack Compose. Ưu điểm chính là khả năng tương thích hoàn toàn với mô hình Compose: composable() hoạt động trong cùng vòng đời với các thành phần Compose khác, mà không cần FragmentManager hay giao dịch. Điều này loại bỏ một lớp lỗi liên quan đến sự không khớp vòng đời giữa Fragment và Compose.
Mỗi composable() nhận một route chuỗi và một hàm lambda nhận đối tượng NavBackStackEntry và trả về UI Composable. Bên trong lambda, có thể truy cập NavController thông qua navController từ phạm vi, cho phép điều hướng đến các màn hình khác. Kiến trúc này làm cho việc điều hướng trở nên rõ ràng và có thể dự đoán được.
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(
onNavigateToProfile = {
navController.navigate("profile")
}
)
}
composable("profile") {
ProfileScreen(
onBack = { navController.popBackStack() }
)
}
}
}
Mỗi lần gọi composable() tạo một đỉnh với mã định danh route duy nhất trong đồ thị nội bộ của NavHost. Khi NavController thực thi navigate(), thư viện so sánh route được yêu cầu với tất cả các đỉnh composable đã đăng ký và tìm kết quả khớp. Sau khi khớp, một NavBackStackEntry được tạo, đặt lên ngăn xếp điều hướng và quá trình kết hợp UI bắt đầu.
Triển khai nội bộ của composable() sử dụng cơ chế khởi tạo trễ: việc kết hợp màn hình chỉ xảy ra ở lần điều hướng đầu tiên đến route đó. Điều này có nghĩa là các màn hình mà người dùng chưa từng điều hướng đến không chiếm bộ nhớ và không thực thi bất kỳ mã nào. Cách tiếp cận này cải thiện đáng kể hiệu suất trong các ứng dụng có nhiều màn hình.
Tham số key trong composable() cho phép quản lý việc tạo lại màn hình. Theo mặc định, composable không được tạo lại khi điều hướng lặp lại đến cùng một route — NavHost sử dụng mục nhập ngăn xếp hiện có. Tuy nhiên, nếu key được truyền và thay đổi, NavHost sẽ tạo một phiên bản mới của hàm composable. Điều này hữu ích cho các màn hình có dữ liệu động cần buộc làm mới trạng thái khi mở lại.
val NavGraphBuilder.Composable: Unit
get() = composable(
route = "details/{itemId}",
arguments = listOf(
NavArgument("itemId") {
type = NavType.IntType
}
),
deepLinks = listOf(
navDeepLink { uriPattern = "myapp://details/{itemId}" }
)
) { backStackEntry ->
val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
DetailsScreen(itemId = itemId)
}
composable() hỗ trợ một hệ thống đối số linh hoạt thông qua tham số arguments. Mỗi đối số được mô tả bằng một đối tượng NavArgument xác định kiểu, giá trị mặc định và tính bắt buộc. Các đối số được truyền trong route dưới dạng tham số đường dẫn (qua dấu ngoặc nhọn) hoặc tham số truy vấn (qua dấu hỏi).
Tham số đường dẫn được chỉ định trực tiếp trong mẫu route: "profile/{userId}". Khi điều hướng đến "profile/42", NavHost tự động trích xuất giá trị 42 và làm cho nó có thể truy cập được qua backStackEntry.arguments. Tham số truy vấn được thêm vào sau dấu hỏi: "search?query={text}" và cũng được thư viện tự động phân tích cú pháp.
Khi trích xuất đối số, điều quan trọng là kiểm tra tính bắt buộc của tham số qua NavType.isNullableAllowed và cung cấp giá trị mặc định qua NavArgument defaultValue. Nếu thiếu tham số bắt buộc, Navigation Compose sẽ ném ra IllegalArgumentException, ngăn chặn các lỗi tinh vi với route không chính xác.
| Loại đối số | NavType | Ví dụ trong route |
|---|---|---|
| Int | NavType.IntType | "item/{id}" |
| String | NavType.StringType | "user/{name}" |
| Boolean | NavType.BoolType | "filter?enabled={value}" |
| Float | NavType.FloatType | "map/{lat}/{lon}" |
| Long | NavType.LongType | "article/{timestamp}" |
Để truyền các đối tượng phức tạp, nên sử dụng NavType.ParcelableType hoặc NavType.SerializableType. Tuy nhiên, Google khuyên nên giảm thiểu kích thước dữ liệu truyền — tốt hơn là truyền một mã định danh và tải đối tượng theo ID bên trong màn hình. Điều này ngăn ngừa các vấn đề với dữ liệu tuần tự hóa lớn và đơn giản hóa việc xử lý thay đổi cấu hình.
data class Profile(val id: Int, val name: String) : Parcelable
// Điều hướng với dữ liệu tối thiểu
navController.navigate("profile/42")
// Truy xuất đối số trên màn hình
composable(
route = "profile/{userId}",
arguments = listOf(
NavArgument("userId") { type = NavType.IntType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getInt("userId") ?: 0
ProfileDetailScreen(userId = userId)
}
Trong các ứng dụng thực tế, thường cần tổ chức các đồ thị điều hướng lồng nhau — ví dụ, một ngăn xếp màn hình riêng biệt bên trong một tab BottomNavigation. composable() hỗ trợ lồng nhau qua NavHost lồng nhau: bên trong một màn hình composable, bạn có thể khai báo NavHost riêng của mình với một ngăn xếp route độc lập.
Mỗi NavHost lồng nhau có NavController và ngăn xếp lùi riêng. Điều này có nghĩa là điều hướng bên trong một tab không ảnh hưởng đến điều hướng trong các tab khác — người dùng có thể chuyển đổi tự do giữa các tab mà không mất lịch sử điều hướng trong mỗi tab. Kiến trúc này được gọi là Scoped Navigation và được Google khuyến nghị cho các ứng dụng có điều hướng đa cấp phức tạp.
Khi triển khai điều hướng lồng nhau, điều quan trọng là quản lý trạng thái NavController một cách chính xác: mỗi NavHost lồng nhau nên lưu trữ rememberNavController riêng của nó trong phạm vi của hàm composable. Theo Android Developer Summit 2024, hơn 40% ứng dụng Jetpack Compose có ba tab trở lên sử dụng kiến trúc NavHost lồng nhau để cô lập điều hướng giữa các mô-đun.
// NavHost chính với các tab
composable("tabs") {
MainTabsScreen { tab ->
when (tab) {
Tab.Home -> HomeNavGraph()
Tab.Search -> SearchNavGraph()
}
}
}
// Đồ thị lồng nhau trong tab Trang chủ
@Composable
fun HomeNavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home_feed"
) {
composable("home_feed") { FeedScreen() }
composable("home_detail/{postId}") { PostDetailScreen() }
}
}
Trước Jetpack Compose, phương pháp điều hướng tiêu chuẩn trong Android sử dụng Intent và FragmentManager. Intent là một thông báo hệ thống khởi chạy một Activity mới, điều này có nghĩa là tạo lại toàn bộ cây View. Ngược lại, composable() hoạt động bên trong một Activity duy nhất và chỉ đơn giản thay thế một phần của cây Compose, nhanh hơn đáng kể và hiệu quả hơn về bộ nhớ.
Sự khác biệt chính giữa composable() và điều hướng dựa trên Intent:
| Đặc điểm | composable() | Intent / Fragment |
|---|---|---|
| Kiến trúc | Single Activity, cây Compose | Multi Activity, ngăn xếp Fragment |
| Truyền dữ liệu | tham số path/query, ViewModel chia sẻ | Intent extras, Bundle, SharedPreferences |
| Liên kết sâu | Hỗ trợ navDeepLink tích hợp | intent-filter trong tệp kê khai |
| Ngăn xếp lùi | Quản lý tự động popBackStack | FragmentManager.popBackStack() |
| Thời gian chuyển | 5–15 ms (trong tiến trình) | 50–200 ms (có tạo lại) |
Chuyển từ Intent sang composable() không chỉ là thay thế API, mà là sự thay đổi mô hình kiến trúc. Thay vì chỉ định rõ ràng Activity nào nên mở, nhà phát triển mô tả khai báo tất cả các route có thể có tại một nơi, cải thiện khả năng đọc mã và đơn giản hóa việc kiểm thử điều hướng. Theo Google I/O 2024, Jetpack Compose với Navigation Compose giảm 40–60% mã điều hướng so với FragmentManager.
Một trong những lỗi phổ biến nhất là tạo lại NavController trong quá trình kết hợp lại. Nếu NavController được tạo qua rememberNavController() ở cấp composable cha, có thể bị tạo lại khi trạng thái thay đổi, điều hướng sẽ hỏng — lịch sử bị mất. Giải pháp đúng là nâng NavController lên cấp composable ổn định, chẳng hạn như cấp Activity hoặc composable gốc của ứng dụng.
Vấn đề phổ biến thứ hai là kết hợp lại vô hạn trong quá trình điều hướng. Điều này xảy ra khi navController.navigate() được đặt trực tiếp trong thân của hàm composable. Vì điều hướng thay đổi trạng thái của NavHost, nó kích hoạt kết hợp lại, từ đó gọi navigate() một lần nữa, tạo ra một vòng lặp. Tất cả các lệnh gọi điều hướng phải được bọc trong lambda xử lý (onClick, onButtonPressed), không được thực thi trong quá trình kết hợp.
Lỗi thứ ba là xử lý ngăn xếp lùi không đúng khi sử dụng BottomNavigation. Điều hướng đơn giản qua navigate() mỗi khi chuyển tab sẽ thêm một mục nhập mới vào ngăn xếp thay vì quay lại mục hiện có. Đối với BottomNavigation, bạn nên sử dụng navController.navigate() với restoreState = true và launchSingleTop = true, đảm bảo khôi phục trạng thái chính xác khi chuyển tab.
fun NavController.navigateToTab(route: String) {
navigate(route) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
Câu hỏi thường gặp
composable() không phải là chú thích, mà là một hàm mở rộng của NavHost liên kết một route với UI. Một hàm @Composable thông thường chỉ đơn giản mô tả bố cục, trong khi composable() đăng ký bố cục đó trong đồ thị điều hướng với một route được chỉ định, làm cho nó có thể truy cập được để điều hướng qua NavController.
Nên chỉ truyền một mã định danh (ID) qua tham số đường dẫn, và tải đối tượng trên màn hình theo ID qua kho lưu trữ hoặc ViewModel. Nếu bạn vẫn cần truyền đối tượng, hãy sử dụng NavType.ParcelableType, nhưng tránh truyền đối tượng lớn hơn 1 KB — điều này có thể dẫn đến TransactionTooLargeException.
Xoay màn hình gây ra thay đổi cấu hình, theo mặc định sẽ tạo lại Activity. Để giữ nguyên trạng thái của màn hình composable, hãy sử dụng rememberSaveable cho dữ liệu đơn giản hoặc ViewModel với phạm vi của màn hình đó. Navigation Compose khôi phục ngăn xếp lùi sau khi tạo lại, nhưng trạng thái bên trong các hàm composable() bị đặt lại nếu không có rememberSaveable.
Không, composable() là một hàm mở rộng của NavGraphBuilder, chỉ khả dụng bên trong khối NavHost. Để thay thế UI đơn giản mà không cần điều hướng, hãy sử dụng kết xuất có điều kiện (when, if) hoặc AnimatedContent. composable() được thiết kế đặc biệt cho định tuyến với hỗ trợ ngăn xếp lùi và liên kết sâu.
Sử dụng SavedStateHandle bên trong ViewModel: ở lần điều hướng đầu tiên, handle.get("initialized") trả về null; ở điều hướng quay lại, nó trả về giá trị đã lưu. Thay vào đó, hãy phân tích vị trí hiện tại trong ngăn xếp lùi qua navController.previousBackStackEntry — nếu nó là null, đây là màn hình đầu tiên trong ngăn xếp điều hướng.
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