Đ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 phần có thể kết hợp 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ợ tính năng Soạn thư, hãy sử dụng phần phụ thuộc sau trong tệp build.gradle của mô-đun ứng dụng:

Groovy

dependencies {
    def nav_version = "2.5.1"

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

Kotlin

dependencies {
    def nav_version = "2.5.1"

    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. Nó là trạng thái và theo dõi ngăn xếp lui thành phần kết hợp phía sau tạo nên màn hình trong ứng dụng của bạn và trạng thái của mỗi màn hình.

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

val navController = rememberNavController()

Bạn nên tạo NavController ở vị trí trong hệ thống phân cấp sáng tạo của bạn, trong đó tất cả các thành phần có thể kết hợp cần tham chiếu đều có quyền truy cập. Điều này tuân theo các nguyên tắc về kéo trạng thái và cho phép bạn sử dụng NavController cũng như trạng thái mà chúng cung cấp thông qua currentBackStackEntryAsState() để dùng làm nguồn của thông tin thực tế về việc cập nhật các hoạt động bên ngoài màn ,hình. Hãy xem bài viết 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 NavHost liên kết. NavHost liên kết NavController với biểu đồ điều hướng trong đó chỉ định các điểm đến có thể so sánh mà bạn có thể điều hướng. Khi bạn di chuyển giữa các thành phần có thể kết hợp, nội dung của NavHost sẽ tự động kết hợp lại. Mỗi điểm đến có thể kết hợp trong biểu đồ điều hướng của bạn được liên kết với một tuyến đường.

Việc tạo NavHost yêu cầu phải có NavController được tạo trước đó thông qua rememberNavController() và lộ trình của điểm đến bắt đầu trong biểu đồ. Việc tạo NavHost sử dụng cú pháp lambda từ 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 đường và giao diện phải được liên kết với điểm đến:

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

Để chuyển đến điểm đến có thể kết hợp trong biểu đồ điều hướng, bạn phải sử dụng phương thức navigate(). navigate() lấy một thông số String đại diện cho tuyến đường đến. Để di chuyển từ một biểu đồ có thể kết nối trong biểu đồ điều hướng, hãy gọi navigate():

@Composable
fun Profile(navController: NavController) {
    /*...*/
    Button(onClick = { navController.navigate("friendslist") }) {
        Text(text = "Navigate next")
    }
    /*...*/
}

Bạn chỉ nên gọi navigate() dưới dạng một phần của lệnh gọi lại chứ không phải là một phần của bản thân tác vụ có thể kết hợp để tránh gọi navigate() trên mỗi quá trình tạo lại.

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

// 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 popUpTo để biết thêm các trường hợp sử dụng.

Tính năng soạn điều hướng cũng hỗ trợ chuyển đối số giữa các đích đến có thể 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 đường 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. Bạn có thể chỉ định một loại khác bằng cách sử dụng thông số arguments để đặt type:

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

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

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

Để chuyển đối số đến đích, bạn cần thêm giá trị vào tuyến đường thay cho phần giữ chỗ trong lệnh gọi navigate:

navController.navigate("profile/user1234")

Để biết danh sách các loại được hỗ trợ, hãy xem Dữ liệu chuyển giữa các điểm đến.

Thêm đối số tùy chọn

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

  • Bạn phải bao gồm các thông số này bằng cú pháp thông số truy vấn ("?argName={argName}")
  • Nhóm này phải có defaultValue hoặc đã đặt nullability = true (theo mặc định sẽ đặt giá trị mặc định là null)

Điều này có nghĩa là bạn phải thêm tất cả các đối số tùy chọn vào hàm composable() dưới dạng một danh sách:

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

Bây giờ, ngay cả khi không có đối số nào được chuyển đến đích, hệ thống vẫn sử dụng defaultValue của "tôi" thay thế.

Cấu trúc của việc xử lý các đối số thông qua các tuyến nghĩa là các hoạt động cạnh tranh của bạn vẫn hoàn toàn độc lập với chức năng Điều hướng và dễ kiểm tra hơn nhiều.

Điều hướng Compose điều hướng hỗ trợ các đường liên kết sâu ngầm định cũng có thể được xác định là một phần của hàm composable(). Thêm chúng dưới dạng danh sách bằng cách sử dụng 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 phần có thể 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>

Tính năng chỉ đường sẽ tự động đi sâu vào đường liên kết sâu đó khi ứng dụng khác kích hoạt đường liên kết sâu.

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 thích hợp từ một trình 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 điểm đến của đường liên kết sâu.

Điều hướng lồng

Các điểm đến có thể được nhóm thành một biểu đồ lồng để mô-đun hóa 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 điểm đến. Giống như biểu đồ gốc, biểu đồ lồng phải có một điểm đến được xác định là điểm đến bắt đầu theo tuyến đường của biểu đồ. Đây là điểm đến được chuyển đến khi bạn di chuyển đến tuyến đường 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 của riêng họ.

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 tiện ích 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 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 có thể kết hợp, bạn có thể kết nối Điều hướng với các thành phần khác như BottomNavBar. Thao tác này cho phép bạn di chuyển bằng cách chọn các biểu tượng trên thanh dưới cùng.

Để liên kết các mục trong thanh điều hướng ở dưới cùng với các tuyến đường 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ó ở đây, chứa mã tuyến đường và mã tài nguyên của chuỗi cho vị trí xuất hiệ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 đó vào danh sách mà BottomNavigationItem có thể sử dụng:

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

Trong BottomNavigation kết hợp, hãy nhận hàm NavBackStackEntry hiện tại bằng cách sử dụng hàm currentBackStackEntryAsState(). Mục này 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 đường của mặt hàng với tuyến đường của điểm đến hiện tại và điểm đến gốc của mục đó (để xử lý các trường hợp khi bạn đang sử dụng lồng di chuyển) thông qua phương thức trình trợ giúp hierarchy.

Tuyến đường của mục này cũng dùng để kết nối onClick lambda với cuộc gọi tới navigate để cho phép người dùng nhấn vào mục đó chuyển đến mục đó. Bằng cách sử dụng cờ saveStaterestoreState, trạng thái và ngăn xếp phía sau 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 tận dụng phương thức NavController.currentBackStackEntryAsState() để nâng trạng thái navController ra 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 mới nhất.

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

Nếu muốn sử dụng thành phần Điều hướng trong ứng dụng Compose, bạn có hai 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 ứng dụng Compose sử dụng điểm đế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 có thể kết hợp.

Do đó, các ứng dụng kết hợp nên sử dụng thành phần Điều hướng dựa trên mảnh và sử dụng các mảnh để giữ màn hình dựa trên chế độ xem, màn hình Compose và màn hình sử dụng cả chế độ xem và Compose. Khi mỗi mảnh màn hình trong ứng dụng của bạn là một trình bao bọc, có thể kết hợp, bước tiếp theo là liên kết tất cả các màn hình đó với nhau bằng tính năng Compose di chuyển và xóa tất cả các mảnh.

Để thay đổi đích đến bên trong mã Compose, bạn hiển thị các sự kiện mà có thể chuyển cho và tất cả các thành phần có thể chuyển đổi trong hệ thống 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 Soạn thư 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 có thể sử dụng lại và kiểm tra được nhiều hơn.

Thử nghiệm

Bạn nên tách mã Điều hướng khỏi các vị trí xuất hiện có thể sử dụng để bật tính năng thử nghiệm riêng cho từng phần. Bạn có thể tách riêng mã này khỏi NavHost.

Mức độ gián tiếp do composable lambda 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 mã có thể 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 tệp sáng tạo của bạn
  • Chuyển các lambda nên được kích hoạt bởi nhà sáng tạo để điều hướng, thay vì chính NavController.

Ví dụ: một tệp sáng tạo Profile có thể 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
) {
 …
}

Ở đây, chúng ta thấy rằng Profile có thể kết hợp hoạt động độc lập với Điều hướng, cho phép thử nghiệm độc lập. Lambda composable sẽ đóng gói logic tối thiểu cần thiết để làm cầu nối giữa các API Điều hướng và tệp sáng tạo của bạn:

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

Tìm hiểu thêm

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

Để tìm hiểu cách thiết kế cách di chuyển trong ứng dụng của bạn sao cho phù hợp với các kích thước màn hình, hướng và các yếu tố biểu mẫu khác nhau, hãy xem Thao tác cho giao diện người dùng thích ứng.