Điều hướng với Compose

Thành phần Điều hướng hỗ trợ các ứng dụng Jetpack Compose. Bạn có thể di chuyển giữa các thành phần kết hợp (composable) trong khi tận dụng cơ sở hạ tầng và các tính năng của thành phần Điều hướng.

Thiết lập

Để hỗ trợ Compose, hãy sử dụng phần phụ thuộc sau đây trong tệp build.gradle của mô-đun ứng dụng:

Groovy

dependencies {
    def nav_version = "2.5.3"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    val nav_version = "2.5.3"

    implementation("androidx.navigation:navigation-compose:$nav_version")
}

Bắt đầu

NavController là API trung tâm cho thành phần Điều hướng. API này có trạng thái và theo dõi ngăn xếp lui của những thành phần kết hợp tạo nên các màn hình trong ứng dụng cũng như trạng thái của từng màn hình.

Bạn có thể tạo NavController bằng cách sử dụng phương thức rememberNavController() trong thành phần kết hợp:

val navController = rememberNavController()

Bạn nên tạo NavController ở vị trí trong hệ phân cấp thành phần kết hợp của mình. Tại đây, mọi thành phần kết hợp cần tham chiếu đến NavController đều có quyền truy cập vào NavController. Điều này tuân theo các nguyên tắc chuyển trạng thái lên trên (state hoisting) và cho phép bạn sử dụng NavController cũng như trạng thái mà nó cung cấp thông qua currentBackStackEntryAsState() để dùng làm nguồn xác thực cập nhật các thành phần kết hợp bên ngoài màn hình. Hãy xem phần Tích hợp với thanh điều hướng dưới cùng để biết ví dụ về chức năng này.

Tạo một NavHost

Mỗi NavController phải được liên kết với một thành phần kết hợp NavHost duy nhất. NavHost liên kết với NavController bằng một biểu đồ điều hướng, có tác dụng chỉ định các đích đến thành phần kết hợp mà bạn có thể điều hướng giữa chúng. Khi bạn điều hướng giữa các thành phần kết hợp, nội dung của NavHost sẽ tự động kết hợp lại. Mỗi thành phần kết hợp đóng vai trò điểm đến trong biểu đồ điều hướng của bạn được liên kết với một tuyến (route).

Việc tạo NavHost cần phải có NavController được tạo trước đó thông qua rememberNavController() và tuyến của đích đến bắt đầu trong biểu đồ. Việc tạo NavHost sử dụng cú pháp lambda của DSL Kotlin điều hướng để tạo biểu đồ điều hướng. Bạn có thể thêm vào cấu trúc điều hướng bằng cách sử dụng phương thức composable(). Phương thức này yêu cầu bạn cung cấp tuyến và thành phần kết hợp phải được liên kết với đích đến:

NavHost(navController = navController, startDestination = "profile") {
    composable("profile") { Profile(/*...*/) }
    composable("friendslist") { FriendsList(/*...*/) }
    /*...*/
}

Để điều hướng đến thành phần kết hợp đóng vai trò điểm đến trong biểu đồ điều hướng, bạn phải sử dụng phương thức navigate. navigate lấy một tham số String duy nhất đại diện cho tuyến của điểm đến. Để điều hướng từ một thành phần kết hợp trong biểu đồ điều hướng, hãy gọi navigate:

navController.navigate("friendslist")

Theo mặc định, navigate sẽ thêm điểm đến mới vào ngăn xếp lui. Bạn có thể sửa đổi hành vi của navigate bằng cách đính kèm các tuỳ chọn điều hướng bổ sung vào lệnh gọi navigate():

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

Hãy xem hướng dẫn sử dụng popUpTo để biết các trường hợp sử dụng khác.

Hàm navigate của NavController sửa đổi trạng thái nội bộ của NavController. Để tuân thủ nhiều nhất có thể với nguyên tắc nguồn đáng tin cậy duy nhất, chỉ hàm có khả năng kết hợp hoặc phần tử giữ trạng thái nâng thực thể NavController và hàm có khả năng kết hợp lấy NavController làm tham số mới phải thực hiện lệnh gọi điều hướng. Những sự kiện điều hướng được kích hoạt qua các hàm có khả năng kết hợp khác ở cấp thấp hơn trong hệ phân cấp giao diện người dùng cần hiển thị các sự kiện đó đến phương thức gọi một cách thích hợp bằng cách sử dụng các hàm.

Ví dụ sau đây cho thấy hàm có khả năng kết hợp MyAppNavHost là nguồn đáng tin cậy duy nhất cho thực thể NavController. ProfileScreen hiển thị một sự kiện dưới dạng hàm được gọi khi người dùng nhấn vào một nút. MyAppNavHost (sở hữu phương thức điều hướng đến nhiều loại màn hình trong ứng dụng) giúp thực hiện lệnh gọi điều hướng tới điểm đến thích hợp khi gọi ProfileScreen.

@Composable
fun MyAppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = "profile"
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        composable("profile") {
            ProfileScreen(
                onNavigateToFriends = { navController.navigate("friendsList") },
                /*...*/
            )
        }
        composable("friendslist") { FriendsListScreen(/*...*/) }
    }
}

@Composable
fun ProfileScreen(
    onNavigateToFriends: () -> Unit,
    /*...*/
) {
    /*...*/
    Button(onClick = onNavigateToFriends) {
        Text(text = "See friends list")
    }
}

Bạn chỉ nên gọi navigate() trong lệnh gọi lại chứ không phải là trên chính thành phần kết hợp để tránh phải gọi navigate() trên từng quá trình tái kết hợp.

Việc hiển thị các sự kiện từ các hàm có khả năng kết hợp cho những phương thức gọi biết cách xử lý một logic cụ thể trong ứng dụng là một phương pháp hay để chuyển trạng thái lên trên trong Compose.

Mặc dù việc cho thấy sự kiện dưới dạng tham số lambda riêng lẻ có thể làm quá tải chữ ký hàm, nhưng việc này sẽ tối đa hoá khả năng hiển thị trong chức năng của hàm có khả năng kết hợp. Bạn có thể xem nhanh về chức năng này.

Các giải pháp thay thế khác có thể làm giảm số lượng tham số trong phần khai báo hàm; trước mắt thì việc viết mã có thể trở nên dễ dàng hơn nhưng tiềm ẩn một số nhược điểm về lâu dài. Ví dụ: tạo một lớp trình bao bọc như ProfileScreenEvents để tập trung tất cả sự kiện ở cùng một nơi. Thao tác này làm bạn khó nắm được hoạt động của thành phần kết hợp khi nhìn vào định nghĩa hàm. Thao tác này cũng thêm các lớp và phương thức khác vào dự án. Ngoài ra, bạn cần tạo và ghi nhớ các thực thể của lớp đó mỗi lần gọi hàm có khả năng kết hợp. Ngoài ra, để sử dụng lại lớp trình bao bọc đó nhiều nhất có thể, mẫu này khuyến khích truyền một thực thể của lớp đó xuống hệ phân cấp giao diện người dùng thay vì phương pháp tốt nhất là chỉ truyền cho các thành phần kết hợp những gì cần thiết.

Thành phần Điều hướng trong Compose cũng hỗ trợ truyền đối số giữa các đích đến có khả năng kết hợp. Để thực hiện việc này, bạn cần thêm trình giữ chỗ đối số vào tuyến của mình, tương tự như cách bạn thêm đối số vào đường liên kết sâu khi sử dụng thư viện điều hướng cơ sở:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

Theo mặc định, tất cả các đối số được phân tích cú pháp dưới dạng chuỗi. Tham số arguments của composable() chấp nhận danh sách NamedNavArgument. Bạn có thể nhanh chóng tạo NamedNavArgument bằng phương thức navArgument rồi chỉ định chính xác type:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

Bạn nên trích xuất đối số từ NavBackStackEntry có trong lambda của hàm composable().

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Để truyền đối số đến đích, bạn cần thêm đối số đó vào tuyến đường khi thực hiện lệnh gọi navigate:

navController.navigate("profile/user1234")

Để biết danh sách các loại được hỗ trợ, hãy xem bài viết Truyền dữ liệu giữa các đích đến.

Truy xuất dữ liệu phức tạp khi điều hướng

Bạn không nên truyền các đối tượng dữ liệu phức tạp khi điều hướng, mà thay vào đó hãy truyền thông tin tối thiểu cần thiết, chẳng hạn như giá trị nhận dạng duy nhất hoặc hình thức mã nhận dạng khác, làm đối số khi thực hiện các thao tác điều hướng:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

Các đối tượng phức tạp phải được lưu trữ dưới dạng dữ liệu trong một nguồn đáng tin cậy, chẳng hạn như lớp dữ liệu. Khi đã tới đích đến sau khi điều hướng, bạn có thể tải thông tin cần thiết từ một nguồn đáng tin cậy thông qua mã nhận dạng được truyền. Để truy xuất các đối số trong ViewModel chịu trách nhiệm truy cập vào lớp dữ liệu, bạn có thể sử dụng ViewModel’s SavedStateHandle:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

Cách này giúp ngăn chặn tình trạng mất dữ liệu trong quá trình thay đổi cấu hình và mọi sự không nhất quán khi đối tượng được đề cập đang được cập nhật hoặc thay đổi.

Để hiểu rõ hơn về lý do bạn nên tránh truyền dữ liệu phức tạp làm đối số, cũng như danh sách các loại đối số được hỗ trợ, hãy xem bài viết Truyền dữ liệu giữa các đích đến.

Thêm đối số không bắt buộc

Điều hướng Compose cũng hỗ trợ các đối số điều hướng không bắt buộc. Các đối số không bắt buộc khác với các đối số bắt buộc theo 2 cách:

  • Bạn phải đưa các đối số này vào bằng cú pháp tham số truy vấn ("?argName={argName}")
  • Các đối số này phải có tập hợp defaultValue hoặc có nullability = true (theo mặc định sẽ đặt giá trị mặc định là null)

Tức là bạn phải thêm tất cả đối số không bắt buộc vào hàm composable() một cách rõ ràng dưới dạng một danh sách:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Giờ đây, ngay cả khi không có đối số nào được truyền đến đích thì hệ thống vẫn sử dụng defaultValue "user1234".

Cấu trúc của quá trình xử lý đối số thông qua tuyến nghĩa là thành phần kết hợp vẫn hoàn toàn độc lập với thành phần Điều hướng và giúp các thành phần này dễ kiểm thử hơn nhiều.

Thành phần Điều hướng trong Compose hỗ trợ các đường liên kết sâu ngầm ẩn cũng có thể được xác định dưới dạng một phần của hàm composable(). Tham số deepLinks của thành phần Điều hướng chấp nhận danh sách các NavDeepLink có thể được tạo nhanh bằng cách sử dụng phương thức navDeepLink:

val uri = "https://www.example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

Các đường liên kết sâu này cho phép bạn liên kết một URL, hành động và/hoặc loại MIME cụ thể với một thành phần kết hợp. Theo mặc định, các đường liên kết sâu này không hiển thị với các ứng dụng bên ngoài. Để đặt các đường liên kết sâu này ở bên ngoài, bạn phải thêm các phần tử <intent-filter> thích hợp vào tệp manifest.xml của ứng dụng. Để bật đường liên kết sâu ở trên, bạn nên thêm nội dung sau đây bên trong phần tử <activity> của tệp kê khai:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

Thành phần điều hướng sẽ tự động liên kết sâu vào các thành phần kết hợp đó khi đường liên kết sâu được một ứng dụng khác kích hoạt.

Bạn cũng có thể sử dụng các đường liên kết sâu này để tạo PendingIntent bằng đường liên kết sâu phù hợp từ một thành phần kết hợp:

val id = "exampleId"
val context = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

Sau đó, bạn có thể sử dụng deepLinkPendingIntent này như bất kỳ PendingIntent nào khác để mở ứng dụng của mình tại đích đến của đường liên kết sâu.

Thành phần Điều hướng được lồng

Các đích đến có thể được nhóm thành một biểu đồ lồng để mô-đun hoá một quy trình cụ thể trong giao diện người dùng của ứng dụng. Ví dụ về quy trình này có thể là quy trình đăng nhập độc lập.

Biểu đồ lồng đóng gói các đích đến. Giống như biểu đồ gốc, biểu đồ lồng phải có một đích đến được xác định là đích đến bắt đầu theo tuyến của biểu đồ. Đây là đích được điều hướng đến khi bạn di chuyển đến tuyến liên kết với biểu đồ lồng.

Để thêm biểu đồ lồng vào NavHost, bạn có thể sử dụng hàm mở rộng navigation:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

Bạn nên chia biểu đồ điều hướng thành nhiều phương thức khi biểu đồ tăng kích thước. Điều này cũng cho phép nhiều mô-đun đóng góp biểu đồ điều hướng riêng.

fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}

Bằng cách đặt phương thức này làm phương thức mở rộng trên NavGraphBuilder, bạn có thể sử dụng phương thức này cùng với các phương thức mở rộng navigation, composabledialog đã tạo sẵn:

NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}

Tích hợp với thanh điều hướng dưới cùng

Bằng cách xác định NavController ở cấp cao hơn trong hệ phân cấp thành phần kết hợp, bạn có thể kết nối thành phần Điều hướng với các thành phần khác như thành phần điều hướng dưới cùng. Thao tác này cho phép bạn điều hướng bằng cách chọn các biểu tượng trên thanh dưới cùng.

Để sử dụng các thành phần BottomNavigationBottomNavigationItem, hãy thêm phần phụ thuộc androidx.compose.material vào ứng dụng Android.

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.3.1"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.2"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.3.1")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.4.2"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Để liên kết các mục trong thanh điều hướng dưới cùng với các tuyến trong biểu đồ điều hướng, bạn nên xác định một lớp kín, chẳng hạn như Screen được thấy ở đây, chứa mã tài nguyên Chuỗi và tuyến cho các đích đến.

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

Sau đó, hãy đặt các mục này vào danh sách mà BottomNavigationItem có thể sử dụng:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

Trong thành phần kết hợp BottomNavigation, hãy sử dụng hàm currentBackStackEntryAsState() để nhận NavBackStackEntry hiện tại. Mục nhập này cung cấp cho bạn quyền truy cập vào NavDestination hiện tại. Sau đó, bạn có thể xác định trạng thái đã chọn của từng BottomNavigationItem bằng cách so sánh tuyến của mục với tuyến của đích đến hiện tại và đích đến gốc của mục đó (để xử lý các trường hợp khi bạn đang sử dụng thành phần điều hướng được lồng) thông qua hệ phân cấp NavDestination.

Tuyến của mục này cũng được dùng để kết nối hàm lambda onClick với lệnh gọi đến navigate sao cho thao tác nhấn vào mục sẽ điều hướng đến mục đó. Bằng cách sử dụng cờ saveStaterestoreState, trạng thái và ngăn xếp lui của mục đó sẽ được lưu và khôi phục chính xác khi bạn hoán đổi giữa các mục điều hướng dưới cùng.

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) { innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { FriendsList(navController) }
  }
}

Tại đây, bạn sẽ tận dụng phương thức NavController.currentBackStackEntryAsState() để nâng trạng thái navController khỏi hàm NavHost và chia sẻ với thành phần BottomNavigation. Điều này có nghĩa là BottomNavigation tự động có trạng thái cập nhật mới nhất.

An toàn về loại cho thành phần Điều hướng trong Compose

Mã trên trang này không an toàn về loại. Bạn có thể gọi hàm navigate() bằng các tuyến hiện không tồn tại hay các đối số không chính xác. Tuy nhiên, bạn có thể định cấu trúc mã Navigation (Điều hướng) để được an toàn về loại vào thời gian chạy. Nhờ vậy, bạn có thể tránh sự cố và đảm bảo rằng:

  • Các đối số, mà bạn cung cấp khi di chuyển đến một đích đến hoặc biểu đồ điều hướng, đều thuộc loại phù hợp và có tất cả đối số bắt buộc.
  • Các đối số bạn truy xuất từ SavedStateHandle có loại chính xác.

Để biết thêm thông tin về yêu cầu này, hãy xem bài viết Tài liệu liên quan đến an toàn về loại cho chức năng điều hướng.

Khả năng tương tác

Nếu muốn sử dụng thành phần Điều hướng trong Compose, bạn có 2 lựa chọn:

  • Xác định biểu đồ điều hướng có thành phần Điều hướng cho các mảnh.
  • Xác định biểu đồ điều hướng có NavHost trong Compose bằng cách sử dụng đích đến Compose. Điều này chỉ có thể xảy ra nếu tất cả các màn hình trong biểu đồ điều hướng đều có thể kết hợp.

Do đó, ứng dụng Compose và Khung hiển thị kết hợp nên sử dụng Thành phần điều hướng dựa trên Mảnh. Sau đó, Mảnh sẽ giữ lại các màn hình dựa trên Khung hiển thị, màn hình Compose và màn hình sử dụng cả Khung hiển thị và Compose. Khi nội dung của mỗi Mảnh nằm trong Compose, bước tiếp theo là liên kết tất cả màn hình với nhau bằng thành phần Điều hướng trong Compose rồi xoá mọi Mảnh.

Để thay đổi điểm đến bên trong mã Compose, bạn hiển thị các sự kiện có thể chuyển tới cũng như kích hoạt bởi thành phần kết hợp bất kỳ trong hệ phân cấp:

@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

Trong phân đoạn của bạn, bạn tạo cầu nối giữa Compose và thành phần Điều hướng dựa trên phân đoạn bằng cách tìm NavController và điều hướng đến đích:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

Ngoài ra, bạn có thể chuyển NavController xuống theo hệ thống phân cấp của Compose. Tuy nhiên, việc hiển thị các hàm đơn giản sẽ giúp dễ sử dụng lại và kiểm thử hơn.

Kiểm thử

Bạn nên tách mã Điều hướng khỏi các đích đến thành phần kết hợp để cho phép kiểm thử riêng từng thành phần kết hợp, tách biệt với thành phần kết hợp NavHost.

Điều này có nghĩa là bạn không nên truyền navController trực tiếp vào thành phần kết hợp nào mà thay vào đó, hãy truyền lệnh gọi lại điều hướng dưới dạng tham số. Điều này cho phép kiểm thử riêng lẻ tất cả các thành phần kết hợp của bạn, vì chúng không yêu cầu thực thể của navController trong kiểm thử.

Cấp độ gián tiếp do lambda composable cung cấp là yếu tố cho phép bạn tách riêng mã Điều hướng khỏi chính thành phần kết hợp này. Cách này hoạt động theo hai hướng:

  • Chỉ chuyển đối số được phân tích cú pháp vào thành phần kết hợp của bạn
  • Chuyển các lambda nên được thành phần kết hợp kích hoạt để điều hướng, thay vì chính NavController.

Ví dụ: một thành phần kết hợp Profile lấy userId làm dữ liệu đầu vào và cho phép người dùng chuyển đến trang hồ sơ của một người bạn có thể có chữ ký:

@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

Theo đó, thành phần kết hợp Profile hoạt động độc lập với thành phần Điều hướng nên có thể được kiểm thử độc lập. Lambda composable sẽ đóng gói logic tối thiểu cần thiết để thu hẹp khoảng cách giữa các Navigation API (API Điều hướng) và thành phần kết hợp của bạn:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "user1234" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

Bạn nên viết các kiểm thử đáp ứng các yêu cầu điều hướng trong ứng dụng bằng cách kiểm thử NavHost, các thao tác điều hướng được truyền đến thành phần kết hợp cũng như các thành phần kết hợp màn hình riêng lẻ.

Kiểm thử NavHost

Để bắt đầu kiểm thử NavHost, hãy thêm phần phụ thuộc kiểm thử điều hướng sau:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

Bạn có thể thiết lập đối tượng kiểm thử NavHost và truyền một thực thể của thực thể navController đến đối tượng đó. Để làm được điều này, cấu phần phần mềm kiểm thử thành phần Điều hướng sẽ cung cấp TestNavHostController. Quy trình kiểm thử giao diện người dùng xác minh đích bắt đầu của ứng dụng và NavHost sẽ có dạng như sau:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

Kiểm thử thao tác điều hướng

Bạn có thể kiểm thử hoạt động triển khai tính năng điều hướng theo nhiều cách, chẳng hạn như nhấp vào thành phần trên giao diện người dùng, sau đó xác minh đích đến đã hiển thị hoặc bằng cách so sánh tuyến dự kiến với tuyến hiện tại.

Khi muốn kiểm thử việc triển khai ứng dụng cụ thể của mình, bạn nên kiểm thử theo cách nhấp vào giao diện người dùng. Để tìm hiểu cách kiểm thử một cách độc lập điều này cùng với các hàm có khả năng kết hợp riêng lẻ, hãy nhớ tham khảo lớp học lập trình Kiểm thử trong Jetpack Compose.

Bạn cũng có thể sử dụng navController để kiểm tra các câu nhận định của mình bằng cách so sánh tuyến đường Chuỗi hiện tại với tuyến đường dự kiến bằng cách sử dụng currentBackStackEntry của navController:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

Để biết thêm hướng dẫn các kiến thức cơ bản về kiểm thử trong Compose, hãy xem tài liệu về Kiểm thử trong Compose và lớp học lập trình Kiểm thử trong Jetpack Compose. Để tìm hiểu thêm về kiểm thử nâng cao đối với mã điều hướng, vui lòng tham khảo hướng dẫn Kiểm thử thành phần Navigation (Điều hướng).

Tìm hiểu thêm

Để tìm hiểu thêm về thành phần Điều hướng trong Jetpack, hãy xem Bắt đầu với thành phần Điều hướng hoặc tham gia Lớp học lập trình về thành phần Điều hướng trong Jetpack Compose.

Để tìm hiểu cách thiết kế tính năng điều hướng trong ứng dụng sao cho phù hợp với nhiều kích thước màn hình, hướng và các hệ số hình dạng, hãy xem bài viết Điều hướng trên giao diện người dùng thích ứng.

Để tìm hiểu thêm về cách triển khai nâng cao hơn cho thành phần Điều hướng trong Compose trong một ứng dụng được mô-đun hoá, bao gồm cả các khái niệm như biểu đồ lồng và tích hợp thanh điều hướng dưới cùng, hãy tham khảo kho lưu trữ Now in Android.

Mẫu