composable(): NavHost và định tuyến trong Jetpack Compose

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

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à hàm đăng ký màn hình trong NavHost của thư viện Navigation Compose.
  • Route — mỗi màn hình được xác định bằng một route chuỗi được truyền làm đối số đầu tiên.
  • Tham số — composable() hỗ trợ đối số qua NavArgument, bao gồm cả bắt buộc và tùy chọn.
  • Lồng nhau — điều hướng lồng nhau được hỗ trợ thông qua NavHost lồng nhau với đồ thị route riêng biệt.
  • Hiệu suất — composable() sử dụng khởi tạo trễ: màn hình chỉ được tạo khi điều hướng lần đầu.

composable() trong NavHost là gì

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.

kotlin
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(
        navController = navController,
        startDestination = "home"
    ) {
        composable("home") {
            HomeScreen(
                onNavigateToProfile = {
                    navController.navigate("profile")
                }
            )
        }
        composable("profile") {
            ProfileScreen(
                onBack = { navController.popBackStack() }
            )
        }
    }
}

Cách composable() hoạt động: khóa và tham số

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.

kotlin
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)
    }

Truyền đối số qua composable()

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ốNavTypeVí dụ trong route
IntNavType.IntType"item/{id}"
StringNavType.StringType"user/{name}"
BooleanNavType.BoolType"filter?enabled={value}"
FloatNavType.FloatType"map/{lat}/{lon}"
LongNavType.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.

kotlin
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)
}

Điều hướng lồng nhau với composable()

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.

kotlin
// 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() }
    }
}

So sánh composable() với điều hướng Intent

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:

  • Tốc độ — composable() chuyển đổi màn hình trong mili giây mà không cần tạo lại Activity; Intent yêu cầu khởi động lại Activity.
  • Hoạt ảnh — trong Navigation Compose, hoạt ảnh chuyển tiếp được định nghĩa khai báo qua AnimatedNavHost, không cần overridePendingTransition.
  • Trạng thái chia sẻ — composable() hoạt động trong phạm vi ViewModel chia sẻ, đơn giản hóa việc truyền dữ liệu giữa các màn hình mà không cần Intent extras.
Đặc điểmcomposable()Intent / Fragment
Kiến trúcSingle Activity, cây ComposeMulti Activity, ngăn xếp Fragment
Truyền dữ liệutham số path/query, ViewModel chia sẻIntent extras, Bundle, SharedPreferences
Liên kết sâuHỗ trợ navDeepLink tích hợpintent-filter trong tệp kê khai
Ngăn xếp lùiQuản lý tự động popBackStackFragmentManager.popBackStack()
Thời gian chuyển5–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.

Lỗi thường gặp với composable()

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.

kotlin
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

Sự khác biệt giữa composable() và hàm @Composable thông thường là gì?

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.

Làm thế nào để truyền một đối tượng phức tạp giữa các màn hình composable()?

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.

Tại sao màn hình composable() bị tạo lại khi xoay màn hình?

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.

Có thể sử dụng composable() mà không có NavHost không?

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.

Làm thế nào để phân biệt lần điều hướng đầu tiên với điều hướng quay lại trong composable()?

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

  • composable() là hàm đăng ký màn hình trong NavHost, cách chính để tổ chức điều hướng trong Jetpack Compose.
  • Route — mỗi màn hình được xác định bằng một chuỗi route với tham số đường dẫn và truy vấn tùy chọn.
  • Đối số — được truyền qua NavArgument với hỗ trợ cho kiểu nguyên thủy, Parcelable và Serializable.
  • Lồng nhau — composable() hỗ trợ NavHost lồng nhau để tổ chức điều hướng mô-đun với các ngăn xếp độc lập.
  • Hiệu suất — khởi tạo màn hình trễ tiết kiệm bộ nhớ, tốc độ chuyển đổi màn hình là 5–15 ms.
  • Lỗi — vấn đề chính: tạo lại NavController, kết hợp lại vô hạn với navigate() trong thân composable, xử lý BottomNavigation không đúng.
  • Di chuyển — chuyển từ FragmentManager sang composable() giảm khối lượng mã điều hướng 40–60% và loại bỏ một lớp lỗi liên quan đến vòng đời Fragment.

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