Thành phần Navigation (Điều hướng) của Jetpack Compose

1. Giới thiệu

Lần cập nhật gần đây nhất: ngày 25 tháng 07 năm 2022

Bạn cần

Navigation (Điều hướng) là một thư viện Jetpack, cho phép bạn di chuyển từ một điểm đích này tới một điểm đích khác trong ứng dụng của mình. Thư viện Navigation cũng cung cấp một cấu phần phần mềm cụ thể, cho phép điều hướng bằng Jetpack Composemột cách rõ ràng và nhất quán. Cấu phần mềm này (navigation-compose) là trọng tâm của lớp học lập trình này.

Bạn sẽ thực hiện

Bạn sẽ dùng nghiên cứu về giao diện Material của Rally làm cơ sở cho lớp học lập trình này để triển khai thành phần điều hướng của Jetpack và cho phép điều hướng giữa các màn hình có khả năng kết hợp của ứng dụng Rally.

Kiến thức bạn sẽ học được

  • Kiến thức cơ bản về cách sử dụng thành phần Jetpack Navigation trong Jetpack Compose
  • Điều hướng giữa các thành phần kết hợp (composable)
  • Tích hợp một thành phần điều hướng có thể kết hợp, dạnh thẻ tuỳ chỉnh vào hệ thống điều hướng
  • Điều hướng chứa đối số
  • Điều hướng thông qua liên kết sâu
  • Kiểm thử tính năng điều hướng

2. Thiết lập

Để làm theo, hãy sao chép điểm xuất phát (nhánh main) của lớp học lập trình này.

$ git clone https://github.com/googlecodelabs/android-compose-codelabs.git

Ngoài ra, bạn có thể tải 2 tệp zip xuống:

Sau khi tải mã xuống, bạn hãy mở thư mục dự án NavigationCodelab trong Android Studio. Bây giờ, bạn đã sẵn sàng để bắt đầu.

3. Tổng quan về ứng dụng Rally

Bước đầu tiên, bạn sẽ làm quen với ứng dụng Rally và cơ sở mã của nó. Chạy và khám phá sơ về ứng dụng.

Rally có ba màn hình chính dưới dạng các thành phần kết hợp:

  1. OverviewScreen — mô tả tổng quan về tất cả thông báo và giao dịch tài chính
  2. AccountsScreen — thông tin chi tiết về các tài khoản hiện có
  3. BillsScreen — khoản chi phí theo kế hoạch

Ảnh chụp màn hình Overivew chứa thông tin về Alerts (Cảnh báo), Accounts (Tài khoản) và Bills (Hoá đơn). Ảnh chụp màn hình Accounts chứa thông tin về một số tài khoản. Ảnh chụp màn hình Bills chứa thông tin về một số hoá đơn gửi đi.

Ở đầu màn hình, Rally hiện sử dụng một thành phần điều hướng có thể kết hợp, dạnh thẻ tuỳ chỉnh (RallyTabRow) để di chuyển giữa 3 màn hình này. Thao tác nhấn vào từng biểu tượng sẽ mở rộng lựa chọn hiện tại và đưa bạn đến một màn hình tương ứng.

336ba66858ae3728.png e26281a555c5820d.png

Khi điều hướng đến màn hình kết hợp này, bạn cũng có thể coi chúng là các đích đến điều hướng, vì chúng ta muốn truy cập vào từng đích tại một điểm cụ thể. Các đích đến này được xác định trước trong tệp RallyDestinations.kt.

Bên trong, bạn sẽ thấy cả ba đích đến chính được xác định là đối tượng (Overview, AccountsBills) cũng như SingleAccount sẽ được thêm vào ứng dụng sau này. Mỗi đối tượng mở rộng từ giao diện RallyDestination, và chứa thông tin cần thiết về từng đích đến cho mục đích điều hướng:

  1. Một icon cho thanh trên cùng
  2. Chuỗi route (cần thiết cho Compose Navigation làm đường dẫn đến đích đó)
  3. Một screen đại diện cho toàn bộ thành phần kết hợp của đích đến này

Khi chạy ứng dụng, bạn sẽ nhận thấy là thực sự có thể di chuyển giữa các đích đến hiện sử dụng thanh trên cùng. Tuy nhiên, trên thực tế, ứng dụng không sử dụng thành phần Navigation (Điều hướng) trong Compose mà thay vào đó, cơ chế điều hướng hiện tại dựa vào một số thao tác chuyển đổi thủ công giữa các thành phần kết hợp và kích hoạt tính năng kết hợp lại để hiển thị nội dung mới. Do đó, mục tiêu của lớp học lập trình này là di chuyển và triển khai thành công Compose Navigation.

4. Di chuyển đến Compose Navigation

Để di chuyển cơ bản sang Jetpack Compose, hãy làm theo các bước sau:

  1. Thêm phần phụ thuộc Navigation Compose mới nhất
  2. Thiết lập NavController
  3. Thêm một NavHost rồi tạo biểu đồ điều hướng
  4. Chuẩn bị tuyến đường để di chuyển giữa các đích đến khác nhau trong ứng dụng
  5. Thay thế cơ chế điều hướng hiện tại bằng tính năng Compose Navigation

Hãy cùng xem chi tiết các bước này.

Thêm phần phụ thuộc Navigation

Mở tệp bản dựng của ứng dụng tại app/build.gradle. Trong mục phần phụ thuộc, hãy thêm phần phụ thuộc navigation-compose.

dependencies {
  implementation "androidx.navigation:navigation-compose:{latest_version}"
  // ...
}

Bạn có thể tìm phiên bản navigation-compose mới nhất tại đây.

Bây giờ, bạn có thể đồng bộ hoá dự án và bắt đầu sử dụng thành phần Navigation trong Compose.

Thiết lập NavController

NavController là thành phần trung tâm khi sử dụng thành phần Navigation trong Compose. Thành phần này giúp theo dõi các mục nhập có thể kết hợp của ngăn xếp lui, di chuyển ngăn xếp tiến, cho phép thao tác ngăn xếp lui và di chuyển giữa các trạng thái của đích đến. Vì NavController là trọng tâm của điều hướng, bạn phải tạo nó ở bước đầu tiên trong việc thiết lập Compose Navigation.

Nhận được NavController bằng cách gọi hàm rememberNavController(). Thao tác này sẽ tạo và ghi nhớ một NavController vẫn tồn tại sau khi thay đổi cấu hình (sử dụng rememberSaveable).

Bạn phải luôn tạo và đặt NavController ở cấp cao nhất trong hệ phân cấp có thể kết hợp, thường nằm trong thành phần kết hợp App của bạn. Theo đó, tất cả thành phần kết hợp cần tham chiếu đến NavController đều có quyền truy cập. Đ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à đảm bảo NavController là nguồn đáng tin cậy chính để di chuyển giữa các màn hình có thể kết hợp cũng như để duy trì ngăn xếp lui.

Mở RallyActivity.kt. Tìm nạp NavController bằng rememberNavController() trong RallyApp vì đây là thành phần kết hợp gốc và điểm truy cập cho toàn bộ ứng dụng:

import androidx.navigation.compose.rememberNavController
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            // ...
        ) {
            // ...
       }
}

Tuyến đường trong Compose Navigation

Như đã đề cập trước đó, Ứng dụng Rally có ba đích đến chính và một đích đến khác sẽ được thêm vào sau này (SingleAccount). Các giá trị này được quy định trong RallyDestinations.kt. và chúng tôi đã đề cập về việc mỗi đích có một icon, routescreen đã xác định:

Ảnh chụp màn hình Overivew chứa thông tin về Alerts (Cảnh báo), Accounts (Tài khoản) và Bills (Hoá đơn). Ảnh chụp màn hình Accounts chứa thông tin về một số tài khoản. Ảnh chụp màn hình Bills chứa thông tin về một số hoá đơn gửi đi.

Bước tiếp theo là thêm các đích đến này vào biểu đồ điều hướng, trong đó Overview là đích đến bắt đầu khi khởi chạy ứng dụng.

Khi sử dụng thành phần Navigation trong Compose, mỗi đích đến có thể kết hợp trong biểu đồ điều hướng của bạn sẽ được liên kết với một tuyến đường. Các tuyến đường được biểu thị dưới dạng Chuỗi xác định đường dẫn đến thành phần kết hợp và hướng dẫn navController truy cập vào đúng vị trí. Bạn có thể coi đây là một đường liên kết sâu ngầm ẩn dẫn đến một điểm đến cụ thể. Mỗi điểm đến phải có một tuyến đường riêng.

Để thực hiện việc này, chúng ta sẽ sử dụng thuộc tính route của mỗi đối tượng RallyDestination. Ví dụ như Overview.route là tuyến đường sẽ đưa bạn đến thành phần kết hợp màn hình Overview.

Gọi thành phần kết hợp NavHost bằng biểu đồ điều hướng

Bước tiếp theo là thêm NavHost và tạo biểu đồ điều hướng.

3 phần chính của thành phần Navigation là NavController, NavGraphNavHost. NavController luôn liên kết với một NavHost có thể kết hợp. NavHost đóng vai trò là vùng chứa và chịu trách nhiệm hiển thị đích đến hiện tại của biểu đồ. 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. Nội dung này cũng liên kết NavController với một biểu đồ điều hướng (NavGraph) xác định các đích đến có thể kết hợp để di chuyển giữa những đích đến này. Về cơ bản, đó là một tập hợp các đích đến có thể tìm nạp được.

Quay lại thành phần kết hợp RallyApp trong RallyActivity.kt. Thay thế thành phần kết hợp Box bên trong Scaffold (chứa nội dung của màn hình hiện tại để chuyển đổi giữa các màn hình theo cách thủ công) bằng một NavHost mới mà bạn có thể tạo bằng cách làm theo ví dụ về mã bên dưới.

Truyền vào tham số navController đã tạo ở bước trước để kết nối với NavHost này. Như đã đề cập trước đó, mỗi NavController phải liên kết với một NavHost duy nhất.

NavHost cũng cần tuyến startDestination để biết đích đến nào sẽ hiển thị khi ứng dụng được khởi chạy, do đó, hãy đặt giá trị này thành Overview.route. Ngoài ra, truyền một Modifier để chấp nhận khoảng đệm Scaffold bên ngoài và áp dụng cho NavHost.

Tham số cuối cùng builder: NavGraphBuilder.() -> Unit chịu trách nhiệm xác định và tạo biểu đồ điều hướng. Hàm này sử dụng cú pháp lambda từ DSL Kotlin điều hướng, vì vậy nó có thể được chuyển dưới dạng trailing lambda (lambda theo sau) bên trong phần nội dung của hàm, và được kéo ra khỏi dấu ngoặc đơn:

import androidx.navigation.compose.NavHost
...

Scaffold(...) { innerPadding ->
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = Modifier.padding(innerPadding)
    ) {
       // builder parameter will be defined here as the graph
    }
}

Thêm đích đến vào NavGraph

Bây giờ, bạn có thể định nghĩa biểu đồ điều hướng và các đích đến mà NavController có thể điều hướng. Như đã đề cập, tham số builder yêu cầu một hàm. Do đó, thành phần Navigation (Điều hướng) trong Compose cung cấp hàm mở rộng NavGraphBuilder.composable để dễ dàng thêm từng đích đến có thể kết hợp vào biểu đồ điều hướng, đồng thời xác định thông tin điều hướng cần thiết.

Đích đầu tiên sẽ là Overview. Vì vậy, bạn cần thêm đích này thông qua hàm mở rộng composable và đặt Chuỗi duy nhất route. Thao tác này chỉ thêm đích đến vào biểu đồ điều hướng. Vì vậy, bạn cũng cần xác định giao diện người dùng thực tế sẽ hiển thị khi di chuyển đến đích này. Bạn cũng có thể thực hiện việc này thông qua trailing lambda (lambda theo sau) bên trong phần thân hàm composable, là một mẫu thường dùng trong Compose:

import androidx.navigation.compose.composable
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
}

Theo mẫu này, chúng ta sẽ thêm cả ba thành phần kết hợp màn hình chính dưới dạng ba đích đến:

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
    composable(route = Accounts.route) {
        Accounts.screen()
    }
    composable(route = Bills.route) {
        Bills.screen()
    }
}

Giờ thì hãy chạy ứng dụng – bạn sẽ thấy Overview làm đích bắt đầu và giao diện người dùng tương ứng hiển thị.

Chúng ta đã đề cập trước thành phần kết hợp thanh tuỳ chỉnh, RallyTabRow, trước đó đã xử lý việc điều hướng theo cách thủ công giữa các màn hình. Tại thời điểm này, trang web chưa được kết nối với tính năng điều hướng mới, vì vậy, bạn có thể xác minh rằng việc nhấp vào thẻ sẽ không thay đổi đích đến của thành phần kết hợp màn hình được hiển thị. Hãy khắc phục lỗi đó trong bước tiếp theo!

5. Tích hợp RallyTabRow với tính năng điều hướng

Trong bước này, bạn sẽ kết nối RallyTabRow với navController và biểu đồ điều hướng để cho phép điều hướng đến đúng đích đến.

Để làm việc này, bạn cần sử dụng navController mới để xác định hành động điều hướng chính xác cho lệnh gọi lại onTabSelected của RallyTabRow. Lệnh gọi lại này xác định những gì sẽ xảy ra khi một biểu tượng thẻ cụ thể được chọn, sau đó thực hiện thao tác điều hướng thông qua navController.navigate(route).

Hãy làm theo hướng dẫn này trong RallyActivity, tìm thành phần kết hợp RallyTabRow và tham số gọi lại onTabSelected của thành phần đó.

Vì chúng ta muốn thẻ điều hướng đến một đích đến cụ thể khi được nhấn, nên bạn cũng cần biết biểu tượng tab chính xác nào đã được chọn. Thật may là tham số onTabSelected: (RallyDestination) -> Unit đã cung cấp điều này. Bạn sẽ sử dụng thông tin đó và tuyến RallyDestination để hướng dẫn navController, đồng thời gọi navController.navigate(newScreen.route) khi một thẻ được chọn:

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    // Pass the callback like this,
                    // defining the navigation action when a tab is selected:
                    onTabSelected = { newScreen ->
                        navController.navigate(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }

Nếu chạy ứng dụng ngay, bạn có thể xác minh rằng thao tác nhấn vào từng thẻ trong RallyTabRow thực sự điều hướng đến đúng đích đến có thể kết hợp. Tuy nhiên, hiện có hai vấn đề mà bạn có thể nhận thấy:

  1. Việc nhấn vào cùng một thẻ trong một hàng sẽ khởi chạy nhiều bản sao có cùng một đích
  2. Giao diện người dùng của thẻ không khớp với đích đến được hiển thị – nghĩa là việc mở rộng và thu gọn các thẻ đã chọn không hoạt động như dự kiến:

336ba66858ae3728.png e26281a555c5820d.png

Hãy khắc phục cả hai vấn đề này!

Ra mắt một bản sao của đích đến

Để khắc phục sự cố đầu tiên và đảm bảo sẽ có tối đa một bản sao của một đích đến nhất định ở đầu ngăn xếp lui, Compose Navigation API sẽ cung cấp cờ launchSingleTop mà bạn có thể truyền đến hành động navController.navigate(), như bên dưới:

navController.navigate(route) { launchSingleTop = true }

Vì bạn muốn hành vi này trên ứng dụng cho mọi đích đến, thay vì sao chép, hãy dán cờ này vào tất cả đích đến của bạn .Khi navigate(...) gọi, bạn có thể trích xuất tiện ích mở rộng này vào tiện ích trợ giúp ở cuối RallyActivity:

import androidx.navigation.NavHostController
// ...

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }

Giờ đây, bạn có thể thay thế lệnh gọi navController.navigate(newScreen.route) bằng .navigateSingleTopTo(...). Chạy lại ứng dụng và xác minh rằng bạn sẽ chỉ nhận được một bản sao của một đích đến khi nhấp nhiều lần vào biểu tượng của đích đến đó ở thanh trên cùng:

@Composable
fun RallyApp() {
    RallyTheme {
        var currentScreen: RallyDestination by remember { mutableStateOf(Overview) }
        val navController = rememberNavController()
        Scaffold(
            topBar = {
                RallyTabRow(
                    allScreens = rallyTabRowScreens,
                    onTabSelected = { newScreen ->
                        navController
                            .navigateSingleTopTo(newScreen.route)
                    },
                    currentScreen = currentScreen,
                )
            }

Kiểm soát các chế độ điều hướng và trạng thái ngăn xếp lui

Ngoài launchSingleTop, bạn cũng có thể sử dụng các cờ khác từ NavOptionsBuilder để kiểm soát và tuỳ chỉnh thêm hành vi điều hướng. Vì RallyTabRow hoạt động tương tự như một BottomNavigation, nên bạn cũng nên cân nhắc xem có muốn lưu và khôi phục trạng thái của đích đến khi di chuyển đến và từ đích đến đó hay không. Ví dụ như nếu bạn cuộn xuống cuối phần Tổng quan rồi chuyển đến Tài khoản và quay lại, bạn có muốn giữ vị trí cuộn không? Bạn có muốn nhấn lại vào cùng một đích đến trong RallyTabRow để tải lại trạng thái màn hình hay không? Đây đều là những câu hỏi xác đáng và cần được xác định theo các yêu cầu thiết kế đối với ứng dụng của riêng bạn.

Chúng tôi sẽ đề cập đến một số tuỳ chọn bổ sung mà bạn có thể sử dụng trong cùng một hàm mở rộng navigateSingleTopTo:

  • launchSingleTop = true – như đã đề cập, điều này đảm bảo sẽ có tối đa một bản sao của một đích nhất định ở đầu ngăn xếp lui
  • Trong ứng dụng Rally, điều này có nghĩa là việc nhấn lại vào cùng một thẻ nhiều lần sẽ không khởi chạy nhiều bản sao của cùng một đích đến
  • popUpTo(startDestination) { saveState = true } – bật đích đến bắt đầu của biểu đồ lên để tránh tạo một ngăn xếp lớn các đích đến trên ngăn xếp lui khi bạn chọn các thẻ
  • Trong Rally, điều này có nghĩa là việc nhấn mũi tên quay lại từ bất kỳ đích đến nào sẽ kéo toàn bộ ngăn xếp lui về mục Overview (Tổng quan)
  • restoreState = true – xác định xem thao tác điều hướng này có khôi phục mọi trạng thái đã lưu trước đó bằng PopUpToBuilder.saveState hay thuộc tính popUpToSaveState hay không. Lưu ý là nếu trước đó không có trạng thái nào được lưu bằng mã đích đến, thì điều này không có hiệu lực
  • Trong Rally, điều này có nghĩa là việc nhấn lại vào cùng một thẻ sẽ giữ lại dữ liệu và trạng thái trước đó của người dùng trên màn hình mà không cần tải lại.

Bạn có thể thêm lần lượt tất cả tuỳ chọn này vào mã, chạy ứng dụng sau mỗi tuỳ chọn và xác minh hành vi chính xác sau khi thêm từng cờ. Bằng cách đó, bạn sẽ có thể xem thực tế cách mỗi cờ thay đổi trạng thái điều hướng và ngăn xếp lui:

import androidx.navigation.NavHostController
import androidx.navigation.NavGraph.Companion.findStartDestination
// ...

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) {
        popUpTo(
            this@navigateSingleTopTo.graph.findStartDestination().id
        ) {
            saveState = true
        }
        launchSingleTop = true
        restoreState = true
}

Sửa giao diện người dùng thẻ

Ngay từ đầu lớp học lập trình, trong khi vẫn sử dụng cơ chế điều hướng thủ công, RallyTabRow đã sử dụng biến currentScreen để xác định xem nên mở rộng hay thu gọn từng thẻ.

Tuy nhiên, sau khi bạn thực hiện các thay đổi, currentScreen sẽ không còn được cập nhật. Đây là lý do tại sao việc mở rộng và thu gọn các thẻ đã chọn bên trong RallyTabRow không hoạt động nữa.

Để kích hoạt lại hành vi này bằng thành phần Navigation (Điều hướng) trong Compose, bạn cần biết đích đến nào đang được hiển thị tại mỗi thời điểm, hoặc theo thuật ngữ về điều hướng thì mục nào đang ở trên cùng ngăn xếp lui, sau đó cập nhật RallyTabRow mỗi khi đích đến thay đổi.

Để nhận thông tin cập nhật theo thời gian thực về đích đến hiện tại từ ngăn xếp lui dưới dạng State, bạn có thể sử dụng navController.currentBackStackEntryAsState() rồi lấy destination: hiện tại

import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()

        val currentBackStack by navController.currentBackStackEntryAsState()
        // Fetch your currentDestination:
        val currentDestination = currentBackStack?.destination
        // ...
    }
}

currentBackStack?.destination trả về NavDestination.. Để cập nhật lại currentScreen đúng cách, bạn cần tìm cách kết hợp NavDestination trở lại với một trong ba thành phần kết hợp màn hình chính của Rally. Bạn phải xác định đích đến nào đang hiển thị để có thể truyền thông tin này đến RallyTabRow.. Như đã đề cập trước đó, mỗi đích đến có một tuyến duy nhất, do đó, chúng ta có thể sử dụng tuyến String (Chuỗi) này làm mã nhận dạng sắp xếp để thực hiện phép so sánh được xác minh và tìm được kết quả tương ứng duy nhất.

Để cập nhật currentScreen, bạn cần lặp lại danh sách rallyTabRowScreens để tìm tuyến đường trùng khớp, sau đó trả về RallyDestination tương ứng. Kotlin cung cấp một hàm .find() hữu ích cho việc đó:

import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.compose.runtime.getValue
// ...

@Composable
fun RallyApp() {
    RallyTheme {
        val navController = rememberNavController()

        val currentBackStack by navController.currentBackStackEntryAsState()
        val currentDestination = currentBackStack?.destination

        // Change the variable to this and use Overview as a backup screen if this returns null
        val currentScreen = rallyTabRowScreens.find { it.route == currentDestination?.route } ?: Overview
        // ...
    }
}

currentScreen đã được chuyển đến RallyTabRow, nên bạn có thể chạy ứng dụng này và xác minh giao diện người dùng thanh thẻ hiện đang được cập nhật tương ứng.

6. Trích xuất các thành phần kết hợp màn hình từ RallyDestinations

Cho đến nay, để đơn giản, chúng tôi đã sử dụng thuộc tính screen từ giao diện RallyDestination và các đối tượng màn hình mở rộng từ giao diện này, để thêm giao diện người dùng có thể kết hợp trong NavHost (RallyActivity.kt):

import com.example.compose.rally.ui.overview.OverviewScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        Overview.screen()
    }
    // ...
}

Tuy nhiên, các bước sau trong lớp học lập trình này (chẳng hạn như sự kiện nhấp chuột) sẽ yêu cầu chuyển trực tiếp thông tin bổ sung đến màn hình có thể kết hợp. Trong môi trường phát hành, chắc chắn sẽ có nhiều dữ liệu hơn cần được truyền.

Để đạt được mục tiêu này, bạn nên thêm các thành phần kết hợp trực tiếp vào biểu đồ điều hướng NavHost và trích xuất các thành phần đó từ RallyDestination. Sau đó, RallyDestination và các đối tượng màn hình sẽ chỉ chứa thông tin dành riêng cho việc điều hướng, như iconroute, và sẽ được tách riêng khỏi mọi nội dung liên quan đến giao diện người dùng trong Compose.

Mở RallyDestinations.kt. Trích xuất thành phần kết hợp của mỗi màn hình từ tham số screen của đối tượng RallyDestination, và vào các hàm composable tương ứng trong NavHost, thay thế lệnh gọi .screen() trước đó, như sau:

import com.example.compose.rally.ui.accounts.AccountsScreen
import com.example.compose.rally.ui.bills.BillsScreen
import com.example.compose.rally.ui.overview.OverviewScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    composable(route = Overview.route) {
        OverviewScreen()
    }
    composable(route = Accounts.route) {
        AccountsScreen()
    }
    composable(route = Bills.route) {
        BillsScreen()
    }
}

Tại thời điểm này, bạn có thể yên tâm để xoá tham số screen khỏi RallyDestination và các đối tượng của tham số đó:

interface RallyDestination {
    val icon: ImageVector
    val route: String
}

/**
 * Rally app navigation destinations
 */
object Overview : RallyDestination {
    override val icon = Icons.Filled.PieChart
    override val route = "overview"
}
// ...

Chạy lại ứng dụng và xác minh mọi thứ vẫn hoạt động như trước đây. Giờ khi đã hoàn tất bước này, bạn có thể thiết lập các sự kiện nhấp chuột bên trong màn hình có thể kết hợp.

Triển khai sự kiện nhấp vào màn hình Overview

Hiện tại, mọi sự kiện nhấp chuột trong OverviewScreen đều bị bỏ qua. Điều này có nghĩa là các nút Tài khoản và hóa đơn "SEE ALL" (XEM TẤT CẢ) có thể nhấp được, nhưng trên thực tế, bạn không được đưa đến bất kỳ nơi nào. Mục tiêu của bước này là bật thao tác di chuyển cho những sự kiện nhấp chuột này.

Ghi lại màn hình tổng quan, cuộn đến các đích nhấp cuối cùng và thử nhấp vào. Thao tác nhấp không hoạt động vì chưa được triển khai.

Thành phần kết hợp OverviewScreen có thể chấp nhận một số hàm làm lệnh gọi lại để đặt làm sự kiện nhấp. Trong trường hợp này, phải có các thao tác điều hướng đưa bạn đến AccountsScreen hoặc BillsScreen. Hãy truyền các lệnh gọi lại điều hướng này đến onClickSeeAllAccountsonClickSeeAllBills để điều hướng đến các đích đến liên quan.

Mở RallyActivity.kt, tìm OverviewScreen trong NavHost và truyền navController.navigateSingleTopTo(...) vào cả hai lệnh gọi lại điều hướng với các tuyến đường tương ứng:

OverviewScreen(
    onClickSeeAllAccounts = {
        navController.navigateSingleTopTo(Accounts.route)
    },
    onClickSeeAllBills = {
        navController.navigateSingleTopTo(Bills.route)
    }
)

navController hiện sẽ có đủ thông tin, chẳng hạn như tuyến đường của điểm đến chính xác, để chuyển đến đích đến phù hợp bằng một lượt nhấp vào nút. Nếu xem xét cách triển khai OverviewScreen, bạn sẽ thấy các lệnh gọi lại này đang được đặt thành các tham số onClick tương ứng:

@Composable
fun OverviewScreen(...) {
    // ...
    AccountsCard(
        onClickSeeAll = onClickSeeAllAccounts,
        onAccountClick = onAccountClick
    )
    // ...
    BillsCard(
        onClickSeeAll = onClickSeeAllBills
    )
}

Như đã đề cập trước đó, hãy giữ navController ở cấp cao nhất trong hệ phân cấp điều hướng, đồng thời di chuyển lên cấp của thành phần kết hợp App (thay vì truyền trực tiếp vào đó, ví dụ: OverviewScreen), giúp bạn dễ dàng xem trước, sử dụng lại và kiểm thử thành phần kết hợp OverviewScreen một cách riêng biệt mà không cần phải dựa vào thực thể navController thực tế hoặc mô phỏng. Việc chuyển lệnh gọi lại thay vào đó cũng cho phép thay đổi nhanh sự kiện nhấp chuột của bạn!

7. Điều hướng đến SingleAccountScreen có đối số

Hãy thêm một số chức năng mới vào màn hình AccountsOverview! Hiện tại, những màn hình này hiển thị một danh sách một số loại tài khoản – "Kiểm tra", "Tiết kiệm tiền mua nhà", v.v.

2f335ceab09e449a.png 2e78a5e090e3fccb.png

Tuy nhiên, việc nhấp vào các loại tài khoản này vẫn chưa có tác dụng nào. Hãy khắc phục vấn đề này. Khi nhấn vào từng loại tài khoản, chúng ta muốn hiển thị một màn hình mới có thông tin chi tiết đầy đủ về tài khoản. Để làm như vậy, chúng tôi cần cung cấp thêm thông tin cho navController về loại tài khoản chính xác đang nhấp vào. Bạn có thể thực hiện việc này qua các đối số.

Đối số là một công cụ rất mạnh mẽ, cho phép định tuyến động bằng cách truyền một hoặc nhiều đối số đến một tuyến đường. Cho phép hiển thị nhiều thông tin dựa trên các đối số khác nhau được cung cấp.

Trong RallyApp, hãy thêm một đích đến mới là SingleAccountScreen. Đích đến này sẽ xử lý việc hiển thị các tài khoản cá nhân này trong biểu đồ bằng cách thêm một hàm composable mới vào NavHost: hiện có

import com.example.compose.rally.ui.accounts.SingleAccountScreen
// ...

NavHost(
    navController = navController,
    startDestination = Overview.route,
    modifier = Modifier.padding(innerPadding)
) {
    ...
    composable(route = SingleAccount.route) {
        SingleAccountScreen()
    }
}

Thiết lập đích đến SingleAccountScreen

Khi bạn đáp vào SingleAccountScreen, điểm đến này cần có thêm thông tin để biết chính xác loại tài khoản mà địa điểm này sẽ hiển thị khi mở cửa. Chúng ta có thể sử dụng đối số để truyền loại thông tin này. Bạn cần chỉ định về việc tuyến đường của nó cũng cần có đối số {account_type}. Nếu xem RallyDestination và đối tượng SingleAccount của đối tượng này, bạn sẽ thấy đối số này đã được định nghĩa để bạn sử dụng dưới dạng Chuỗi accountTypeArg.

Để truyền đối số cùng với tuyến đường của bạn khi điều hướng, bạn cần nối các đối số đó với nhau, theo mẫu: "route/{argument}". Trong trường hợp của bạn, lệnh sẽ có dạng như sau: "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}". Hãy nhớ ký hiệu $ được dùng để thoát biến:

import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...

composable(
    route =
        "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
) {
    SingleAccountScreen()
}

Việc này sẽ đảm bảo khi một hành động được kích hoạt để điều hướng đến SingleAccountScreen, đối số accountTypeArg cũng phải được truyền, nếu không thì quá trình điều hướng sẽ không thành công. Hãy coi đây là một chữ ký hoặc một hợp đồng cần phải được theo dõi bởi các đích đến khác muốn điều hướng đến SingleAccountScreen.

Bước thứ hai là thiết lập để composable này nhận biết các đối số. Bạn thực hiện điều này bằng cách xác định tham số arguments. Bạn có thể xác định bao nhiêu đối số tùy thích, vì theo mặc định, hàm composable chấp nhận danh sách các đối số. Trong trường hợp của bạn, bạn chỉ cần thêm một chế độ cài đặt có tên là accountTypeArg, sau đó thêm một số dữ liệu an toàn khác bằng cách chỉ định mã này là loại String. Nếu bạn không đặt rõ một kiểu, thì kiểu đó sẽ được suy ra từ giá trị mặc định của đối số này:

import androidx.navigation.NavType
import androidx.navigation.compose.navArgument
// ...

composable(
    route =
        "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
    arguments = listOf(
        navArgument(SingleAccount.accountTypeArg) { type = NavType.StringType }
    )
) {
    SingleAccountScreen()
}

Mã này sẽ hoạt động hoàn hảo và bạn có thể chọn giữ lại mã như thế này. Tuy nhiên, vì mọi thông tin cụ thể về đích đến đều nằm trong RallyDestinations.kt và các đối tượng của nó, nên chúng ta hãy tiếp tục áp dụng cách thức tương tự (như đã thực hiện ở trên đối với Overview, Accounts,Bills), đồng thời di chuyển các đối số này vào SingleAccount:

object SingleAccount : RallyDestination {
    // ...
    override val route = "single_account"
    const val accountTypeArg = "account_type"
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
}

Thay thế các đối số trước đó bằng SingleAccount.arguments hiện quay trở lại composable tương ứng trên NavHost. Việc này cũng đảm bảo là chúng ta sẽ giữ cho NavHost sạch và dễ đọc nhất có thể:

composable(
    route = "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
    arguments =  SingleAccount.arguments
) {
    SingleAccountScreen()
}

Sau khi xác định được tuyến hoàn chỉnh với các đối số cho SingleAccountScreen, bạn hãy đảm bảo rằng accountTypeArg này được tiếp tục truyền đến thành phần kết hợp SingleAccountScreen để thành phần này biết được đâu là loại tài khoản cần hiển thị chính xác. Nếu xem xét cách triển khai SingleAccountScreen, bạn sẽ thấy thiết bị đã được thiết lập và đang chờ chấp nhận tham số accountType:

fun SingleAccountScreen(
    accountType: String? = UserData.accounts.first().name
) {
   // ...
}

Cho đến thời điểm hiện tại:

  • Bạn phải chắc chắn việc xác định tuyến đường để yêu cầu đối số, làm tín hiệu cho các đích đến trước đó
  • Bạn phải đảm bảo composable biết cần phải chấp nhận các đối số

Bước cuối cùng là thực sự truy xuất đối số đã truyền theo cách nào đó.

Trong Navigation Navigation, mỗi hàm có khả năng kết hợp NavHost đều có quyền truy cập vào NavBackStackEntry hiện tại – một lớp chứa thông tin về tuyến đường hiện tại và truyền các đối số của một mục nhập trong ngăn xếp lui. Bạn có thể sử dụng tính năng này để lấy danh sách arguments bắt buộc từ navBackStackEntry, sau đó tìm kiếm và truy xuất chính xác đối số cần thiết để chuyển đối số đó xuống màn hình kết hợp.

Trong trường hợp này, bạn sẽ yêu cầuaccountTypeArg từnavBackStackEntry. Sau đó, bạn cần truyền tiếp tham số đó xuống tham số accountType của SingleAccountScreen'.

Bạn cũng có thể cung cấp một giá trị mặc định cho đối số làm phần giữ chỗ trong trường hợp đối số đó chưa được cung cấp, đồng thời giúp mã của bạn an toàn hơn bằng cách bao gồm trường hợp đặc biệt (edge case) này.

Mã của bạn bây giờ sẽ có dạng như sau:

NavHost(...) {
    // ...
    composable(
        route =
          "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}",
        arguments = SingleAccount.arguments
    ) { navBackStackEntry ->
        // Retrieve the passed argument
        val accountType =
            navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)

        // Pass accountType to SingleAccountScreen
        SingleAccountScreen(accountType)
    }
}

SingleAccountScreen của bạn hiện đã có thông tin cần thiết để hiển thị đúng loại tài khoản khi bạn chuyển đến đó. Nếu xem xét cách triển khai SingleAccountScreen,, bạn có thể thấy tính năng này đã thực hiện việc so khớp accountTypeđược chuyển với nguồn UserData để tìm nạp thông tin chi tiết tương ứng về tài khoản.

Hãy thực hiện thêm một nhiệm vụ tối ưu hoá nhỏ, đồng thời di chuyển tuyến "${SingleAccount.route}/{${SingleAccount.accountTypeArg}}" vào RallyDestinations.kt và đối tượng SingleAccount của tuyến này:

object SingleAccount : RallyDestination {
    // ...
    override val route = "single_account"
    const val accountTypeArg = "account_type"
    val routeWithArgs = "${route}/{${accountTypeArg}}"
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
}

Và một lần nữa, hãy thay thế mã này trong NavHost composable: tương ứng

// ...
composable(
    route = SingleAccount.routeWithArgs,
    arguments = SingleAccount.arguments
) {...}

Thiết lập đích đến cho Tài khoản và Tổng quan

Sau khi bạn đã định nghĩa được tuyến SingleAccountScreen và đối số mà tuyến này yêu cầu, đồng thời chấp nhận thực hiện điều hướng thành công đến SingleAccountScreen, bạn cần đảm bảo đối số accountTypeArg đó đang được truyền từ đích đến trước (nghĩa là một đích đến bắt đầu bất kỳ của bạn).

Như bạn thấy, đối tượng này có 2 phía – đích đến bắt đầu cung cấp và truyền một đối số, còn đích đến kết thúc tiếp nhận và sử dụng đối số đó để hiển thị thông tin chính xác. Bạn cần xác định cả hai.

Ví dụ như khi bạn đang ở đích Accounts và nhấn vào loại tài khoản "Đang kiểm tra", đích Tài khoản cần chuyển một Chuỗi "Đang kiểm tra" làm đối số, nối vào tuyến đường Chuỗi "single_account", để mở thành công SingleAccountScreen tương ứng. Tuyến đường của chuỗi sẽ có dạng như sau: "single_account/Checking"

Bạn sẽ sử dụng cùng một tuyến đường này với đối số được truyền khi sử dụng navController.navigateSingleTopTo(...), như sau:

navController.navigateSingleTopTo("${SingleAccount.route}/$accountType").

Chuyển lệnh gọi lại thao tác điều hướng này đến tham số onAccountClick của OverviewScreenAccountsScreen. Vui lòng lưu ý các tham số này được xác định trước là onAccountClick: (String) -> Unit, trong đó Chuỗi là dữ liệu đầu vào. Tức là khi người dùng nhấn vào một loại tài khoản cụ thể trong OverviewAccount, loại tài khoản đó là Chuỗi sẽ có sẵn cho bạn, cũng như có thể dễ dàng được chuyển dưới dạng đối số điều hướng:

OverviewScreen(
    // ...
    onAccountClick = { accountType ->
        navController
          .navigateSingleTopTo("${SingleAccount.route}/$accountType")
    }
)
// ...

AccountsScreen(
    // ...
    onAccountClick = { accountType ->
        navController
          .navigateSingleTopTo("${SingleAccount.route}/$accountType")
    }
)

Để mọi thứ dễ đọc, bạn có thể trích xuất hành động điều hướng này thành một trình trợ giúp riêng, hàm mở rộng:

import androidx.navigation.NavHostController
// ...
OverviewScreen(
    // ...
    onAccountClick = { accountType ->
        navController.navigateToSingleAccount(accountType)
    }
)

// ...

AccountsScreen(
    // ...
    onAccountClick = { accountType ->
        navController.navigateToSingleAccount(accountType)
    }
)

// ...

private fun NavHostController.navigateToSingleAccount(accountType: String) {
    this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}

Khi chạy ứng dụng tại thời điểm này, bạn có thể nhấp vào từng loại tài khoản và sẽ được chuyển đến SingleAccountScreen tương ứng hiển thị dữ liệu cho tài khoản được chọn.

Ghi lại màn hình tổng quan, cuộn đến các đích nhấp cuối cùng và thử nhấp vào. Bây giờ, các lượt nhấp sẽ dẫn đến các đích.

8. Bật chế độ hỗ trợ liên kết sâu

Ngoài việc thêm các đối số, bạn cũng có thể thêm đường liên kết sâu để 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. Trên Android, đường liên kết sâu là đường đưa bạn đến thẳng một đích đến cụ thể trong ứng dụng. Navigation Compose hỗ trợ đường liên kết sâu ngầm ẩn. Khi đường liên kết sâu ngầm ẩn được gọi (ví dụ như khi người dùng nhấp vào một đường liên kết), Android có thể mở ứng dụng tại đích đến tương ứng.

Trong phần này, bạn sẽ thêm một đường liên kết sâu mới để chuyển đến thành phần kết hợp SingleAccountScreen với loại tài khoản tương ứng, đồng thời cho phép đường liên kết sâu này hiển thị với các ứng dụng bên ngoài. Để làm mới bộ nhớ, tuyến đường cho thành phần kết hợp này là "single_account/{account_type}", đây cũng là những gì bạn sẽ sử dụng cho liên kết sâu, với một số thay đổi nhỏ liên quan đến liên kết sâu.

Vì theo mặc định, tính năng hiển thị đường liên kết sâu đến các ứng dụng bên ngoài chưa được bật, nên bạn cũng phải thêm các phần tử <intent-filter> vào tệp manifest.xml của ứng dụng này. Do đó, đây sẽ là bước đầu tiên của bạn.

Bắt đầu bằng cách thêm đường liên kết sâu vào AndroidManifest.xml của ứng dụng. Bạn cần tạo một bộ lọc ý định mới qua <intent-filter> bên trong <activity>, với thao tác VIEW và các danh mục BROWSABLEDEFAULT.

Sau đó, bên trong bộ lọc, bạn cần thẻ data để thêm scheme (rally – tên của ứng dụng) và host (single_account – định tuyến tới thành phần kết hợp) để xác định đường liên kết sâu chính xác. Thao tác này sẽ cung cấp cho bạn rally://single_account dưới dạng URL liên kết sâu.

Vui lòng lưu ý là bạn không cần khai báo đối số account_type trong AndroidManifest. Mã này sẽ được thêm vào sau trong hàm có khả năng kết hợp NavHost.

<activity
    android:name=".RallyActivity"
    android:windowSoftInputMode="adjustResize"
    android:label="@string/app_name"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="rally" android:host="single_account" />
    </intent-filter>
</activity>

Giờ bạn đã có thể phản hồi ý định đến từ bên trong RallyActivity.

Thành phần kết hợp SingleAccountScreen đã chấp nhận các đối số, nhưng giờ đây cũng cần chấp nhận đường liên kết sâu mới tạo để khởi chạy đích đến này khi đường liên kết sâu của nó được kích hoạt.

Bên trong hàm kết hợp của SingleAccountScreen, hãy thêm một tham số deepLinks nữa. Tương tự như arguments,, thuộc tính này cũng chấp nhận danh sách navDeepLink, vì bạn có thể xác định nhiều đường liên kết sâu dẫn đến cùng một đích đến. Truyền uriPattern phù hợp với một thuộc tính được xác định trongintent-filter trong tệp kê khai - rally://singleaccount, nhưng lần này bạn cũng sẽ thêm đối số accountTypeArg của nó:

import androidx.navigation.navDeepLink
// ...

composable(
    route = SingleAccount.routeWithArgs,
    // ...
    deepLinks = listOf(navDeepLink {
        uriPattern = "rally://${SingleAccount.route}/{${SingleAccount.accountTypeArg}}"
    })
)

Bạn biết các thao tác tiếp theo chứ? Di chuyển danh sách này vào RallyDestinations SingleAccount:

object SingleAccount : RallyDestination {
    // ...
    val arguments = listOf(
        navArgument(accountTypeArg) { type = NavType.StringType }
    )
    val deepLinks = listOf(
       navDeepLink { uriPattern = "rally://$route/{$accountTypeArg}"}
    )
}

Và một lần nữa, hãy thay thế mã này trong thành phần kết hợp NavHost tương ứng

// ...
composable(
    route = SingleAccount.routeWithArgs,
    arguments = SingleAccount.arguments,
    deepLinks = SingleAccount.deepLinks
) {...}

Ứng dụng và SingleAccountScreen hiện đã sẵn sàng để xử lý các đường liên kết sâu. Để kiểm tra xem ứng dụng có hoạt động đúng cách hay không, hãy cài đặt làm mới Rally trên một trình mô phỏng hoặc thiết bị đã kết nối, mở dòng lệnh rồi thực thi lệnh sau để mô phỏng việc chạy đường liên kết sâu:

adb shell am start -d "rally://single_account/Checking" -a android.intent.action.VIEW

Thao tác này sẽ đưa bạn đến thẳng tài khoản "Đang kiểm tra". Tuy nhiên, bạn cũng có thể xác minh việc tài khoản đó hoạt động đúng cách đối với tất cả các loại tài khoản khác.

9. Trích xuất NavHost thành RallyNavHost

Bạn hiện đã hoàn tất thành phần kết hợp NavHost. Tuy nhiên, để hàm này có thể kiểm thử được, đồng thời giữ cho RallyActivity sạch hơn, bạn có thể trích xuất NavHost hiện tại và các hàm trợ giúp (chẳng hạn như navigateToSingleAccount) từ thành phần kết hợp RallyApp thành hàm có khả năng kết hợp và đặt tên là RallyNavHost.

RallyApp là thành phần kết hợp duy nhất sẽ hoạt động trực tiếp với navController. Như đã đề cập trước đó, mọi màn hình kết hợp khác sẽ chỉ nhận lệnh gọi lại điều hướng, không phải chính navController.

Do đó, RallyNavHost mới sẽ chấp nhận navControllermodifier làm tham số từ RallyApp:

@Composable
fun RallyNavHost(
    navController: NavHostController,
    modifier: Modifier = Modifier
) {
    NavHost(
        navController = navController,
        startDestination = Overview.route,
        modifier = modifier
    ) {
        composable(route = Overview.route) {
            OverviewScreen(
                onClickSeeAllAccounts = {
                    navController.navigateSingleTopTo(Accounts.route)
                },
                onClickSeeAllBills = {
                    navController.navigateSingleTopTo(Bills.route)
                },
                onAccountClick = { accountType ->
                   navController.navigateToSingleAccount(accountType)
                }
            )
        }
        composable(route = Accounts.route) {
            AccountsScreen(
                onAccountClick = { accountType ->
                   navController.navigateToSingleAccount(accountType)
                }
            )
        }
        composable(route = Bills.route) {
            BillsScreen()
        }
        composable(
            route = SingleAccount.routeWithArgs,
            arguments = SingleAccount.arguments,
            deepLinks = SingleAccount.deepLinks
        ) { navBackStackEntry ->
            val accountType =
              navBackStackEntry.arguments?.getString(SingleAccount.accountTypeArg)
            SingleAccountScreen(accountType)
        }
    }
}

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) { launchSingleTop = true }

private fun NavHostController.navigateToSingleAccount(accountType: String) {
    this.navigateSingleTopTo("${SingleAccount.route}/$accountType")
}

Giờ thì hãy thêm RallyNavHost mới vào RallyApp và chạy lại ứng dụng để xác minh mọi thứ hoạt động như trước:

fun RallyApp() {
    RallyTheme {
    ...
        Scaffold(
        ...
        ) { innerPadding ->
            RallyNavHost(
                navController = navController,
                modifier = Modifier.padding(innerPadding)
            )
        }
     }
}

10. Kiểm thử tính năng Compose Navigation

Từ đầu lớp học lập trình này, bạn phải đảm bảo không truyền navController trực tiếp vào bất kỳ thành phần kết hợp nào (ngoài ứng dụng cấp cao) và chuyển lệnh gọi lại điều hướng dưới dạng tham số. Điều này cho phép tất cả các thành phần kết hợp đều có thể thử nghiệm riêng lẻ, vì chúng không yêu cầu thực thể navController trong thử nghiệm.

Bạn phải luôn kiểm tra để đảm bảo toàn bộ cơ chế Điều hướng Compose hoạt động như dự kiến trong ứng dụng của mình bằng cách kiểm thử RallyNavHost, và các thao tác điều hướng được chuyển đến nội dung kết hợp. Đây sẽ là những mục tiêu chính của phần này. Để kiểm thử riêng từng hàm có khả năng kết hợp, vui lòng tham khảo lớp học lập trình Kiểm thử trong Jetpack Compose.

Để bắt đầu kiểm thử, trước tiên, chúng ta cần thêm các phần phụ thuộc kiểm thử cần thiết. Vì vậy, hãy quay lại tệp bản dựng của ứng dụng tại app/build.gradle. Trong mục phần phụ thuộc, hãy thêm phần phụ thuộc navigation-testing:

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

Chuẩn bị lớp NavigationTest

Bạn có thể kiểm thử RallyNavHost riêng biệt với Activity.

Vì chương trình kiểm thử này sẽ vẫn chạy trên thiết bị Android, bạn cần tạo thư mục kiểm thử /app/src/androidTest/java/com/example/compose/rally rồi tạo một lớp kiểm thử tệp kiểm thử mới và đặt tên cho lớp đó là NavigationTest.

Trước tiên, để sử dụng các API kiểm thử Compose cũng như kiểm trả và kiểm soát các thành phần kết hợp và ứng dụng bằng Compose, hãy thêm một quy tắc kiểm thử Compose:

import androidx.compose.ui.test.junit4.createComposeRule
import org.junit.Rule

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

}

Viết kiểm thử đầu tiên

Tạo một hàm kiểm thử rallyNavHost công khai và chú thích hàm @Test đó. Trong hàm này, trước tiên bạn cần thiết lập nội dung Compose mà bạn muốn kiểm thử. Bạn sẽ thực hiện việc này bằng cách sử dụng setContent của composeTestRule. Hàm này sẽ lấy một tham số có thể kết hợp làm nội dung, đồng thời cho phép bạn viết mã Compose và thêm các thành phần kết hợp trong môi trường thử nghiệm, giống như khi bạn đang sử dụng một ứng dụng môi trường sản xuất thông thường.

Bên trong setContent,, bạn có thể thiết lập đối tượng kiểm thử hiện tại, RallyNavHost và truyền một thực thể của thực thể navController mới tới đối tượng đó. Cấu phần phần mềm kiểm thử Navigation cung cấp một TestNavHostController hữu ích để sử dụng. Vì thế hãy thêm bước này:

import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.ComposeNavigator
import androidx.navigation.testing.TestNavHostController
import org.junit.Assert.fail
import org.junit.Test
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @Test
    fun rallyNavHost() {
        composeTestRule.setContent {
            // Creates a TestNavHostController
            navController =
                TestNavHostController(LocalContext.current)
            // Sets a ComposeNavigator to the navController so it can navigate through composables
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }
        fail()
    }
}

Nếu bạn sao chép mã ở trên, lệnh gọi fail() sẽ đảm bảo kiểm thử không đạt cho đến khi đưa ra một xác nhận (assertion) thực sự. Lệnh này như một lời nhắc để hoàn tất quá trình kiểm thử.

Để xác minh rằng thành phần kết hợp màn hình được hiển thị là chính xác, bạn có thể sử dụng contentDescription để xác nhận rằng thành phần đó được hiển thị. Trong lớp học lập trình này, trước đây bạn đã thiết lập contentDescription cho tài khoản và đích đến Tổng quan để có thể sử dụng chúng cho quá trình xác minh thử nghiệm.

Với lần xác minh đầu tiên, bạn cần kiểm tra để đảm bảo màn hình Tổng quan được hiển thị làm đích đến đầu tiên khi RallyNavHost được khởi chạy lần đầu. Bạn cũng nên đổi tên bài kiểm thử để phản ánh điều đó – hãy gọi hàm rallyNavHost_verifyOverviewStartDestination. Bạn có thể thực hiện việc này bằng cách thay thế lệnh gọi fail() với lệnh bên dưới:

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
// ...

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()

    lateinit var navController: TestNavHostController

    @Test
    fun rallyNavHost_verifyOverviewStartDestination() {
        composeTestRule.setContent {
            navController =
                TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(
                ComposeNavigator()
            )
            RallyNavHost(navController = navController)
        }

        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Chạy lại bài kiểm thử và xác minh bài kiểm thử đó đạt.

Vì bạn cần thiết lập RallyNavHost theo cách tương tự cho từng kiểm thử sắp tới, bạn có thể trích xuất khởi chạy của hàm này vào hàm @Before có chú thích, để tránh việc lặp lại không cần thiết và giữ cho kiểm thử ngắn gọn hơn:

import org.junit.Before
// ...

class NavigationTest {

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

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

    @Test
    fun rallyNavHost_verifyOverviewStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Overview Screen")
            .assertIsDisplayed()
    }
}

Bạn có thể kiểm thử việc 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 các thành phầ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 đường dự kiến với tuyến đường hiện tại.

Kiểm thử thông qua các lượt nhấp vào giao diện người dùng và contentDescription trên màn hình

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. Văn bản tiếp theo có thể xác minh việc khi ở màn hình Tổng quan, thao tác nhấp vào nút "SEE ALL" (XEM TẤT CẢ) trong tiểu mục Tài khoản sẽ đưa bạn đến đích Tài khoản:

5a9e82acf7efdd5b.png

Bạn sẽ sử dụng lại contentDescription được đặt trên nút cụ thể này trong thành phần kết hợp OverviewScreenCard, mô phỏng một lần nhấp vào nút đó qua performClick() và xác minh rằng đích đến Tài khoản được hiển thị sau đó:

import androidx.compose.ui.test.performClick
// ...

@Test
fun rallyNavHost_clickAllAccount_navigatesToAccounts() {
    composeTestRule
        .onNodeWithContentDescription("All Accounts")
        .performClick()

    composeTestRule
        .onNodeWithContentDescription("Accounts Screen")
        .assertIsDisplayed()
}

Bạn có thể làm theo mẫu này để kiểm tra tất cả các thao tác nhấp còn lại trong ứng dụng.

Kiểm thử thông qua so sánh các lộ trình và lượt nhấp trên giao diện người dùng

Bạn cũng có thể sử dụng navController để kiểm tra xác nhận 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. Để thực hiện việc này, hãy nhấp vào giao diện người dùng giống như trong phần trước, sau đó dùng navController.currentBackStackEntry?.destination?.route để so sánh tuyến hiện tại với tuyến mà bạn mong đợi.

Bạn nên thực hiện thêm một bước nữa, là trước tiên, hãy di chuyển đến mục con Bills trên màn hình Overview (Tổng quan), nếu không thì quá trình kiểm thử sẽ không thành công vì không thể tìm thấy nút có contentDescription "All Bills" (Tất cả hoá đơn):

import androidx.compose.ui.test.performScrollTo
import org.junit.Assert.assertEquals
// ...

@Test
fun rallyNavHost_clickAllBills_navigateToBills() {
    composeTestRule.onNodeWithContentDescription("All Bills")
        .performScrollTo()
        .performClick()

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

Khi làm theo các mẫu này, bạn có thể hoàn tất lớp kiểm thử bằng cách đưa vào mọi tuyến đường điều hướng, đích đến và hành động nhấp bổ sung. Chạy toàn bộ tập hợp kiểm thử ngay để xác minh chúng đã đạt.

11. Xin chúc mừng

Xin chúc mừng, bạn đã hoàn tất thành công lớp học lập trình này! Bạn có thể tìm thấy mã giải pháp tại đây và so sánh với mã giải pháp của mình.

Bạn đã thêm thành phần điều hướng Jetpack Compose vào ứng dụng Rally, đồng thời hiểu rõ các khái niệm chính của ứng dụng. Bạn đã tìm hiểu cách thiết lập biểu đồ điều hướng cho các đích đến có thể kết hợp, xác định các hành động và tuyến điều hướng, truyền thông tin bổ sung đến các tuyến thông qua đối số, thiết lập đường liên kết sâu và kiểm thử các chức năng điều hướng.

Để tìm hiểu về các chủ đề và thông tin khác, chẳng hạn như tích hợp thanh điều hướng dưới cùng, điều hướng nhiều mô-đun và biểu đồ lồng nhau, bạn có thể xem kho lưu trữ dành cho ứng dụng Now in Android trên GitHub và xem cách triển khai tại đó.

Nội dung tiếp theo là gì?

Vui lòng xem các tài liệu này để tiếp tục lộ trình học tập trong Jetpack Compose :

Thông tin khác về Jetpack Navigation (Điều hướng Jetpack):

Tài liệu tham khảo