Di chuyển giữa các màn hình bằng tính năng Compose

1. Trước khi bắt đầu

Cho đến thời điểm này, các ứng dụng bạn đã xử lý đều chỉ có một màn hình. Tuy vậy, rất nhiều ứng dụng có thể có nhiều màn hình để bạn điều hướng. Ví dụ: ứng dụng Cài đặt có nhiều trang nội dung trên nhiều màn hình.

Trang đầu tiên trong ứng dụng Cài đặt Android.

Trang Cài đặt sau khi người dùng chọn "Thiết bị thông minh" trên trang đầu tiên.

Trang Cài đặt sau khi người dùng chọn "Ghép nối thiết bị mới" trên trang trước.

Trong quá trình phát triển Android hiện đại, các ứng dụng nhiều màn hình được tạo bằng thành phần Điều hướng Jetpack. Thành phần Điều hướng (Navigation) trong Compose cho phép bạn dễ dàng xây dựng ứng dụng nhiều màn hình trong Compose bằng phương pháp khai báo, tương tự như việc tạo giao diện người dùng. Lớp học lập trình này giới thiệu thông tin cơ bản của thành phần Điều hướng trong Compose, cách giúp AppBar (Thanh ứng dụng) phản hồi nhanh với thao tác điều hướng và cách gửi dữ liệu từ ứng dụng của bạn đến một ứng dụng khác bằng ý định (thể hiện các phương pháp hay nhất trong một ứng dụng ngày càng phức tạp).

Điều kiện tiên quyết

  • Làm quen với ngôn ngữ Kotlin gồm loại hàm, lambda và hàm phạm vi
  • Làm quen với các bố cục RowColumn cơ bản trong Compose

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

  • Tạo một thành phần kết hợp NavHost để xác định các tuyến và màn hình trong ứng dụng.
  • Di chuyển giữa các màn hình bằng NavHostController.
  • Thao tác với ngăn xếp lui để chuyển về các màn hình trước.
  • Dùng ý định để chia sẻ dữ liệu với một ứng dụng khác.
  • Tuỳ chỉnh AppBar, bao gồm tiêu đề và nút quay lại.

Sản phẩm bạn sẽ tạo ra

  • Bạn sẽ triển khai tính năng điều hướng trong một ứng dụng nhiều màn hình.

Bạn cần có

  • Phiên bản mới nhất của Android Studio
  • Kết nối Internet để tải mã khởi động xuống

2. Tải mã khởi động

Để bắt đầu, hãy tải mã khởi động xuống:

Ngoài ra, bạn có thể sao chép kho lưu trữ GitHub cho mã:

$ git clone
https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout starter

3. Hướng dẫn từng bước về ứng dụng

Ứng dụng Cupcake hơi khác so với các ứng dụng bạn đã từng sử dụng. Thay vì toàn bộ nội dung xuất hiện trên một màn hình, ứng dụng sẽ có bốn màn hình riêng biệt và người dùng có thể di chuyển qua từng màn hình khi đang đặt bánh nướng.

Màn hình bắt đầu đơn đặt hàng

Màn hình đầu tiên cho người dùng thấy ba nút tương ứng với số lượng bánh nướng để đặt hàng.

Màn hình đầu tiên của ứng dụng Cupcake với các lựa chọn để bắt đầu đặt 1, 6 hoặc 12 chiếc bánh nướng.

Trong mã, thành phần này được biểu thị bằng thành phần kết hợp StartOrderScreen trong StartOrderScreen.kt.

Màn hình là một cột duy nhất, có hình ảnh và văn bản, cùng với ba nút tuỳ chỉnh để đặt số lượng bánh nướng nhỏ. Các nút tuỳ chỉnh được triển khai bằng thành phần kết hợp SelectQuantityButton, cũng nằm trong StartOrderScreen.kt.

Chọn màn hình hương vị

Sau khi chọn số lượng, ứng dụng sẽ nhắc người dùng chọn hương vị cho bánh nướng. Ứng dụng dùng nút chọn để cho thấy nhiều lựa chọn. Người dùng có thể chọn một trong số các hương vị có thể chọn.

Ứng dụng Cupcake hiện nhiều lựa chọn về hương vị cho người dùng.

Danh sách các hương vị được lưu trữ dưới dạng danh sách mã nhận dạng tài nguyên chuỗi trong data.DataSource.kt.

Chọn màn hình ngày đến lấy hàng

Sau khi chọn một hương vị, ứng dụng sẽ hiện một loạt các nút chọn khác để người dùng chọn ngày đến lấy hàng. Các lựa chọn đến lấy hàng từ danh sách được hàm pickupOptions() trả về trong OrderViewModel.

Ứng dụng Cupcake hiện các lựa chọn ngày lấy hàng cho người dùng.

Cả màn hình Choose Flavor (Chọn hương vị) và màn hình Choose Pickup Date (Chọn ngày lấy hàng) đều được biểu thị bằng cùng một thành phần kết hợp SelectOptionScreen trong SelectOptionScreen.kt. Tại sao bạn nên sử dụng cùng một thành phần kết hợp? Bố cục của các màn hình này giống hệt nhau! Điểm khác biệt duy nhất giữa các màn hình là dữ liệu, nhưng bạn có thể sử dụng cùng một thành phần kết hợp để cho thấy cả màn hình hương vị lẫn màn hình chọn ngày lấy hàng.

Màn hình Tóm tắt đơn đặt hàng

Sau khi chọn ngày đến lấy hàng, ứng dụng sẽ hiện màn hình Order Summary (Tóm tắt đơn đặt hàng) để người dùng có thể xem lại và hoàn tất đơn đặt hàng.

Ngoài lựa chọn gửi đơn đặt hàng đến một ứng dụng khác hoặc huỷ đơn đặt hàng, ứng dụng Cupcake còn hiện thông tin tóm tắt về đơn đặt hàng, bao gồm số lượng bánh, hương vị, ngày lấy hàng và tổng số tiền.

Màn hình này được thành phần kết hợp OrderSummaryScreen triển khai trong OrderSummaryScreen.kt.

Bố cục bao gồm một Column chứa toàn bộ thông tin về đơn đặt hàng, một thành phần kết hợp Text để tạo tổng giá tiền và các nút dùng để gửi đơn đặt hàng đến một ứng dụng khác hoặc huỷ đơn đặt hàng và quay lại màn hình đầu tiên.

Nếu người dùng chọn gửi đơn đặt hàng đến một ứng dụng khác, thì ứng dụng Cupcake sẽ hiện một bảng dưới cùng cho thấy các lựa chọn chia sẻ.

Ứng dụng Cupcake cho người dùng thấy các lựa chọn chia sẻ như tin nhắn SMS hoặc Email.

Trạng thái hiện tại của ứng dụng được lưu trữ trong data.OrderUiState.kt. Lớp dữ liệu OrderUiState chứa thuộc tính để lưu trữ lựa chọn của người dùng từ mỗi màn hình.

Các màn hình của ứng dụng sẽ được trình bày trong thành phần kết hợp CupcakeApp. Tuy nhiên, trong dự án khởi động, ứng dụng chỉ cho thấy màn hình đầu tiên. Bạn hiện không thể điều hướng qua tất cả các màn hình của ứng dụng, nhưng đừng lo lắng, bạn ở đây để làm điều đó! Bạn sẽ tìm hiểu cách xác định tuyến điều hướng, thiết lập thành phần kết hợp NavHost để điều hướng giữa các màn hình (còn gọi là đích đến), thực hiện ý định tích hợp bằng thành phần giao diện người dùng hệ thống như màn hình chia sẻ, đồng thời giúp AppBar phản hồi các thay đổi về thao tác điều hướng.

Các thành phần kết hợp có thể sử dụng lại

Ứng dụng mẫu trong khoá học này được thiết kế để triển khai các phương pháp hay nhất khi thích hợp. Ứng dụng Cupcake cũng không ngoại lệ. Trong gói ui.components, bạn sẽ thấy một tệp có tên CommonUi.kt chứa thành phần kết hợp FormattedPriceLabel. Nhiều màn hình trong ứng dụng dùng thành phần kết hợp này để định dạng giá của đơn đặt hàng một cách nhất quán. Thay vì sao chép thành phần kết hợp Text với cùng định dạng và đối tượng sửa đổi, bạn có thể xác định FormattedPriceLabel một lần sau đó sử dụng lại cho các màn hình khác khi cần.

Màn hình hương vị và màn hình ngày đến lấy hàng sử dụng thành phần kết hợp SelectOptionScreen. Bạn cũng có thể sử dụng lại thành phần này. Thành phần kết hợp này lấy một tham số có tên là options thuộc loại List<String> đại diện cho các lựa chọn sẽ hiển thị. Những lựa chọn này xuất hiện trong Row, bao gồm một thành phần kết hợp RadioButton và một thành phần kết hợp Text chứa từng chuỗi. Column bao quanh toàn bộ bố cục, đồng thời chứa một thành phần kết hợp Text để hiện giá đã định dạng, nút Cancel (Huỷ) và nút Next (Tiếp theo).

4. Xác định các tuyến và tạo NavHostController

Các phần của Thành phần điều hướng

Thành phần điều hướng có ba phần chính:

  • NavController: Chịu trách nhiệm điều hướng giữa các đích đến, tức là màn hình trong ứng dụng.
  • NavGraph: Bản đồ các đích đến có thể kết hợp để điều hướng đến.
  • NavHost: Thành phần kết hợp đóng vai trò như một vùng chứa để hiện đích đến hiện tại của NavGraph.

Trong lớp học lập trình này, bạn sẽ tập trung vào NavController và NavHost. Trong NavHost, bạn sẽ xác định các đích đến cho NavGraph của ứng dụng Cupcake.

Xác định tuyến cho các đích đến trong ứng dụng của bạn

Một trong những khái niệm cơ bản về tính năng điều hướng trong Compose là tuyến. Tuyến là một chuỗi tương ứng với một đích đến. Ý tưởng này tương tự như khái niệm về URL. Giống như một URL ánh xạ đến một trang khác trên trang web, tuyến là một chuỗi ánh xạ tới một đích đến và đóng vai trò là mã nhận dạng duy nhất. Thông thường, đích đến là một thành phần kết hợp hoặc nhóm các thành phần kết hợp tương ứng với những gì người dùng nhìn thấy. Ứng dụng Cupcake cần các đích đến cho màn hình bắt đầu đặt hàng, màn hình hương vị, màn hình ngày lấy hàng và màn hình tóm tắt đơn đặt hàng.

Ứng dụng có số lượng màn hình giới hạn, theo đó cũng giới hạn về số lượng các tuyến. Bạn có thể xác định các tuyến của một ứng dụng bằng lớp enum. Các lớp enum trong Kotlin có thuộc tính tên trả về một chuỗi có tên thuộc tính.

Bạn sẽ bắt đầu bằng cách xác định 4 tuyến cho ứng dụng Cupcake.

  • Start: Dùng một trong ba nút để chọn số lượng bánh nướng.
  • Flavor: Chọn hương vị trong danh sách các lựa chọn.
  • Pickup: Chọn ngày lấy hàng trong danh sách các lựa chọn.
  • Summary: Xem lại các lựa chọn rồi gửi hoặc huỷ đơn đặt hàng.

Thêm một lớp enum để xác định các tuyến.

  1. Trong CupcakeScreen.kt, phía trên thành phần kết hợp CupcakeAppBar, hãy thêm một lớp enum có tên CupcakeScreen.
enum class CupcakeScreen() {

}
  1. Thêm 4 trường hợp vào lớp enum: Start, Flavor, PickupSummary.
enum class CupcakeScreen() {
    Start,
    Flavor,
    Pickup,
    Summary
}

Thêm NavHost vào ứng dụng

NavHost là một thành phần kết hợp cho thấy các đích đến có thể kết hợp khác, dựa trên một tuyến nhất định. Ví dụ: nếu tuyến là Flavor, thì NavHost sẽ hiển thị màn hình để bạn chọn hương vị bánh nướng. Nếu tuyến là Summary thì ứng dụng sẽ cho thấy màn hình tóm tắt.

Cú pháp cho NavHost cũng giống như mọi Thành phần kết hợp khác.

fae7688d6dd53de9.png

Có hai tham số đáng chú ý.

  • navController: Một bản sao của lớp NavHostController. Bạn có thể sử dụng đối tượng này để điều hướng giữa các màn hình, chẳng hạn như bằng cách gọi phương thức navigate() để điều hướng đến một đích đến khác. Bạn có thể lấy NavHostController bằng cách gọi rememberNavController() từ một hàm có khả năng kết hợp.
  • startDestination: Một tuyến của chuỗi xác định đích đến sẽ xuất hiện theo mặc định khi ứng dụng hiện NavHost lần đầu tiên. Trong trường hợp ứng dụng Cupcake, đây phải là tuyến Start.

Giống như các thành phần kết hợp khác, NavHost cũng lấy tham số modifier.

Bạn sẽ thêm NavHost vào thành phần kết hợp CupcakeApp trong CupcakeScreen.kt. Trước tiên, bạn cần tham chiếu đến trình điều khiển điều hướng. Bạn có thể sử dụng trình điều khiển điều hướng trong cả NavHost bạn đang thêm và AppBar mà bạn sẽ thêm ở bước sau. Do đó, bạn nên khai báo biến trong thành phần kết hợp CupcakeApp().

  1. Mở CupcakeScreen.kt.
  2. Phía trên biến viewModel trong thành phần kết hợp CupcakeApp, hãy tạo một biến mới bằng cách sử dụng val có tên navController và đặt biến bằng với kết quả của lệnh gọi rememberNavController().
@Composable
fun CupcakeApp(modifier: Modifier = Modifier){
    val navController = rememberNavController()

    ...
}
  1. Trong Scaffold, bên dưới biến uiState, hãy thêm một thành phần kết hợp NavHost.
Scaffold(
    ...
) { innerPadding ->
    val uiState by viewModel.uiState.collectAsState()

    NavHost()
}
  1. Truyền biến navController cho tham số navControllerCupcakeScreen.Start.name cho tham số startDestination. Truyền đối tượng sửa đổi đã truyền vào CupcakeApp() cho tham số của đối tượng sửa đổi. Truyền trailing lambda (lambda theo sau) trống cho thông số cuối cùng.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
}

Xử lý các tuyến trong NavHost

Giống như những thành phần kết hợp khác, NavHost nhận một loại hàm cho nội dung.

f67974b7fb3f0377.png

Trong hàm nội dung của NavHost, bạn hãy gọi hàm composable(). Hàm composable() có hai tham số bắt buộc.

  • route: Một chuỗi tương ứng với tên của một tuyến. Đây có thể là một chuỗi duy nhất. Bạn sẽ sử dụng thuộc tính tên của các hằng số enum CupcakeScreen.
  • content: Tại đây, bạn có thể gọi thành phần kết hợp mà bạn muốn trình bày cho tuyến vừa nêu.

Bạn sẽ gọi hàm composable() một lần cho mỗi tuyến.

  1. Gọi hàm composable(), truyền CupcakeScreen.Start.name cho route.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {

    }
}
  1. Trong trailing lambda (lambda theo sau), gọi thành phần kết hợp StartOrderScreen để truyền quantityOptions cho thuộc tính quantityOptions.
NavHost(
   navController = navController,
   startDestination = CupcakeScreen.Start.name,
   modifier = modifier.padding(innerPadding)
) {
    composable(route = CupcakeScreen.Start.name) {
        StartOrderScreen(
            quantityOptions = quantityOptions
        )
    }
}
  1. Bên dưới lệnh gọi đầu tiên tới composable(), hãy gọi lại composable(), truyền CupcakeScreen.Flavor.name cho route.
composable(route = CupcakeScreen.Flavor.name) {

}
  1. Trong trailing lambda, hãy tham chiếu đến LocalContext.current và lưu trữ nó trong một biến có tên là context. Bạn có thể sử dụng biến này để lấy chuỗi từ danh sách mã nhận dạng tài nguyên trong mô hình chế độ xem để hiện danh sách các hương vị.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current

}
  1. Gọi thành phần kết hợp SelectOptionScreen.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(

    )
}
  1. Màn hình hương vị cần hiển thị và cập nhật tổng giá tiền khi người dùng chọn hương vị. Truyền vào uiState.price cho tham số subtotal.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price
    )
}
  1. Màn hình hương vị lấy danh sách các hương vị từ tài nguyên chuỗi của ứng dụng. Tạo một danh sách các chuỗi từ danh sách hương vị trong mô hình chế độ xem. Bạn có thể chuyển đổi danh sách mã nhận dạng tài nguyên thành danh sách các chuỗi bằng cách sử dụng hàm map() và gọi stringResource().
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> stringResource(id) }
    )
}
  1. Đối với tham số onSelectionChanged, hãy truyền biểu thức lambda gọi setFlavor() trên mô hình chế độ xem, truyền vào it (đối số đã truyền vào onSelectionChanged()).
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}

Màn hình ngày lấy hàng cũng tương tự như màn hình hương vị. Điểm khác biệt duy nhất là dữ liệu được truyền vào thành phần kết hợp SelectOptionScreen.

  1. Gọi lại hàm composable(), truyền CupcakeScreen.Pickup.name cho tham số route.
composable(route = CupcakeScreen.Pickup.name) {

}
  1. Trong trailing lambda, hãy gọi thành phần kết hợp SelectOptionScreen và truyền uiState.price vào subtotal như trước. Truyền uiState.pickupOptions cho tham số options và biểu thức lambda gọi setDate() trên viewModel cho tham số onSelectionChanged.
SelectOptionScreen(
    subtotal = uiState.price,
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. Gọi composable() lại một lần nữa, truyền CupcakeScreen.Summary.name cho route.
composable(route = CupcakeScreen.Summary.name) {

}
  1. Trong trailing lambda, hãy gọi thành phần kết hợp OrderSummaryScreen(), truyền vào biến uiState cho tham số orderUiState.
composable(route = CupcakeScreen.Summary.name) {
    OrderSummaryScreen(
        orderUiState = uiState
    )
}

Đó là các bước để thiết lập NavHost. Ở phần tiếp theo, bạn sẽ làm cho ứng dụng thay đổi các tuyến và di chuyển giữa các màn hình khi người dùng nhấn vào từng nút.

5. Di chuyển giữa các tuyến

Bây giờ, khi bạn đã xác định và ánh xạ các tuyến tới thành phần kết hợp trong NavHost, đã đến lúc điều hướng giữa các màn hình. Thuộc tính NavHostController (thuộc tính navController từ việc gọi rememberNavController()) chịu trách nhiệm di chuyển giữa các tuyến. Tuy nhiên, hãy lưu ý rằng, thuộc tính này được xác định trong thành phần kết hợp CupcakeApp. Bạn cần có cách để truy cập ứng dụng từ các màn hình khác nhau trong ứng dụng của mình.

Thật dễ dàng đúng không? Bạn chỉ cần truyền navController dưới dạng tham số cho từng thành phần kết hợp.

Mặc dù có thể dùng phương pháp này, nhưng đây không phải là cách lý tưởng để cấu trúc ứng dụng. Do đó, một trong những lợi ích của việc sử dụng NavHost để xử lý hoạt động điều hướng là logic điều hướng được tách khỏi giao diện người dùng. Tuỳ chọn này tránh được một số hạn chế lớn khi truyền navController dưới dạng tham số.

  • Logic điều hướng được lưu giữ tại cùng địa điểm, giúp mã dễ bảo trì hơn và ngăn lỗi bằng cách không vô tình cung cấp cho các màn hình quyền tự do điều hướng trong ứng dụng.
  • Đối với ứng dụng cần hoạt động trên nhiều kiểu dáng (như điện thoại ở chế độ dọc, điện thoại có thể gập lại hoặc máy tính bảng màn hình lớn), một nút có thể hoặc không thể kích hoạt tính năng điều hướng, tuỳ thuộc vào bố cục ứng dụng. Mỗi màn hình riêng lẻ phải độc lập và không cần nhận biết màn hình khác trong ứng dụng.

Thay vào đó, cách tiếp cận của chúng ta là truyền một loại hàm vào từng thành phần kết hợp cho những gì sẽ xảy ra khi người dùng nhấp vào nút. Theo đó, thành phần kết hợp và bất kỳ thành phần kết hợp con nào của nó sẽ quyết định thời điểm gọi hàm. Tuy nhiên, logic điều hướng không thể hiện trên mỗi màn hình trong ứng dụng. Tất cả hành vi điều hướng đều được xử lý trong NavHost.

Thêm trình xử lý nút vào StartOrderScreen

Bạn sẽ bắt đầu bằng cách thêm một tham số loại hàm được gọi khi người dùng nhấn một trong các nút số lượng ở màn hình đầu tiên. Hàm này được truyền vào thành phần kết hợp StartOrderScreen, chịu trách nhiệm cập nhật viewmodel và chuyển đến màn hình tiếp theo.

  1. Mở StartOrderScreen.kt.
  2. Bên dưới tham số quantityOptions và trước tham số sửa đổi, hãy thêm tham số có tên là onNextButtonClicked thuộc loại () -> Unit.
@Composable
fun StartOrderScreen(
    quantityOptions: List<Pair<Int, Int>>,
    onNextButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
...
}

Mỗi nút tương ứng với một số lượng bánh nướng khác nhau. Bạn sẽ cần thông tin này để hàm được truyền vào onNextButtonClicked có thể cập nhật viewmodel cho phù hợp.

  1. Sửa đổi loại của tham số onNextButtonClicked để lấy tham số Int.
onNextButtonClicked: (Int) -> Unit,

Để Int truyền vào khi gọi onNextButtonClicked(), hãy xem loại tham số quantityOptions.

Loại này là List<Pair<Int, Int>> hoặc danh sách Pair<Int, Int>. Loại Pair có thể nghe xa lạ, nhưng đơn giản chỉ là một cặp giá trị giống như tên gọi của nó. Pair nhận hai tham số loại chung. Trong trường hợp này, cả hai đều thuộc loại Int.

8326701a77706258.png

Mỗi mục trong một cặp được thuộc tính đầu tiên hoặc thuộc tính thứ hai truy cập. Trong trường hợp tham số quantityOptions của thành phần kết hợp StartOrderScreen, Int đầu tiên là mã tài nguyên để chuỗi này hiện trên từng nút. Int thứ hai là số lượng bánh nướng thực tế.

Chúng ta sẽ truyền thuộc tính thứ hai của cặp đã chọn khi gọi hàm onNextButtonClicked().

  1. Truyền một biểu thức lambda vào tham số onClick của SelectQuantityButton.
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = {  }
    )
}
  1. Trong biểu thức lambda, hãy gọi onNextButtonClicked, truyền vào item.second (số lượng bánh nướng).
quantityOptions.forEach { item ->
    SelectQuantityButton(
        labelResourceId = item.first,
        onClick = { onNextButtonClicked(item.second) }
    )
}

Thêm trình xử lý nút vào SelectOptionScreen

  1. Bên dưới tham số onSelectionChanged của thành phần kết hợp SelectOptionScreen trong SelectOptionScreen.kt, hãy thêm một tham số có tên onCancelButtonClicked thuộc loại () -> Unit.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Bên dưới tham số onCancelButtonClicked, hãy thêm một tham số khác thuộc loại () -> Unit có tên onNextButtonClicked.
@Composable
fun SelectOptionScreen(
    subtotal: String,
    options: List<String>,
    onSelectionChanged: (String) -> Unit = {},
    onCancelButtonClicked: () -> Unit = {},
    onNextButtonClicked: () -> Unit = {},
    modifier: Modifier = Modifier
)
  1. Truyền onCancelButtonClicked cho tham số onClick của nút huỷ.
OutlinedButton(modifier = Modifier.weight(1f), onClick = onCancelButtonClicked) {
    Text(stringResource(R.string.cancel))
}
  1. Truyền onNextButtonClicked cho tham số onClick của nút tiếp theo.
Button(
    modifier = Modifier.weight(1f),
    enabled = selectedValue.isNotEmpty(),
    onClick = onNextButtonClicked
) {
    Text(stringResource(R.string.next))
}

Thêm trình xử lý nút vào SummaryScreen

Cuối cùng, hãy thêm các hàm xử lý nút cho nút Cancel (Huỷ) và nút Send (Gửi) trên màn hình tóm tắt.

  1. Trong thành phần kết hợp OrderSummaryScreen trong OrderSummaryScreen.kt, hãy thêm một tham số có tên là onCancelButtonClicked thuộc loại () -> Unit.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Thêm một tham số khác thuộc loại () -> Unit và đặt tên cho tham số này là onSendButtonClicked.
@Composable
fun OrderSummaryScreen(
    orderUiState: OrderUiState,
    onCancelButtonClicked: () -> Unit,
    onSendButtonClicked: (String, String) -> Unit,
    modifier: Modifier = Modifier
){
    ...
}
  1. Truyền onSendButtonClicked cho tham số onClick của nút Send (Gửi). Truyền vào newOrderorderSummary, là hai biến được xác định trước đó trong OrderSummaryScreen. Các chuỗi này bao gồm dữ liệu thực tế mà người dùng có thể chia sẻ với ứng dụng khác.
Button(
    modifier = Modifier.fillMaxWidth(),
    onClick = { onSendButtonClicked(newOrder, orderSummary) }
) {
    Text(stringResource(R.string.send))
}
  1. Truyền onCancelButtonClicked cho tham số onClick của nút Cancel (Huỷ).
OutlinedButton(
    modifier = Modifier.fillMaxWidth(),
    onClick = onCancelButtonClicked
) {
    Text(stringResource(R.string.cancel))
}

Để điều hướng đến một tuyến khác, bạn chỉ cần gọi phương thức navigate() trên bản sao NavHostController.

fc8aae3911a6a25d.png

Phương thức điều hướng sẽ lấy một tham số duy nhất: một chuỗi tương ứng với một tuyến đã xác định trong NavHost. Nếu tuyến này khớp với một trong các lệnh gọi đến composable() trong NavHost, thì ứng dụng sẽ điều hướng đến màn hình đó.

Bạn sẽ truyền các hàm gọi navigate() khi người dùng nhấn vào các nút trên màn hình Start, FlavorPickup.

  1. Trong CupcakeScreen.kt, hãy tìm lệnh gọi đến composable() cho màn hình bắt đầu. Đối với tham số onNextButtonClicked, hãy truyền vào biểu thức lambda.
StartOrderScreen(
    quantityOptions = quantityOptions,
    onNextButtonClicked = {
    }
)

Bạn có nhớ thuộc tính Int được truyền vào hàm này cho số lượng bánh nướng nhỏ không? Trước khi chuyển đến màn hình tiếp theo, bạn nên cập nhật mô hình hiển thị để ứng dụng cho thấy tổng giá tiền chính xác.

  1. Gọi setQuantity trên viewModel, truyền vào it.
onNextButtonClicked = {
    viewModel.setQuantity(it)
}
  1. Gọi navigate() trên navController, truyền vào CupcakeScreen.Flavor.name cho route.
onNextButtonClicked = {
    viewModel.setQuantity(it)
    navController.navigate(CupcakeScreen.Flavor.name)
}
  1. Đối với tham số onNextButtonClicked trên màn hình hương vị, bạn chỉ cần truyền vào một hàm lambda gọi navigate(), truyền vào CupcakeScreen.Pickup.name cho route.
composable(route = CupcakeScreen.Flavor.name) {
    val context = LocalContext.current
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Pickup.name) },
        options = flavors.map { id -> context.resources.getString(id) },
        onSelectionChanged = { viewModel.setFlavor(it) }
    )
}
  1. Truyền vào một hàm lambda trống cho onCancelButtonClicked mà bạn sẽ triển khai tiếp theo.
SelectOptionScreen(
     subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Pickup.name) },
    onCancelButtonClicked = {},
    options = flavors.map { id -> context.resources.getString(id) },
    onSelectionChanged = { viewModel.setFlavor(it) }
)
  1. Đối với tham số onNextButtonClicked trên màn hình đến lấy hàng, hãy truyền vào một hàm lambda gọi navigate(), truyền vào CupcakeScreen.Summary.name cho route.
composable(route = CupcakeScreen.Pickup.name) {
    SelectOptionScreen(
        subtotal = uiState.price,
        onNextButtonClicked = {
            navController.navigate(CupcakeScreen.Summary.name)
        },
        options = uiState.pickupOptions,
        onSelectionChanged = { viewModel.setDate(it) }
    )
}
  1. Một lần nữa, truyền vào một hàm lambda trống cho onCancelButtonClicked().
SelectOptionScreen(
    subtotal = uiState.price,
    onNextButtonClicked = {
        navController.navigate(CupcakeScreen.Summary.name) },
    onCancelButtonClicked = {},
    options = uiState.pickupOptions,
    onSelectionChanged = { viewModel.setDate(it) }
)
  1. Đối với OrderSummaryScreen, hãy truyền các hàm lambda trống vào onCancelButtonClickedonSendButtonClicked. Thêm các tham số cho subjectsummary đã truyền vào onSendButtonClicked. Bạn sẽ sớm triển khai các tham số này.
composable(route = CupcakeScreen.Summary.name) {
   val context = LocalContext.current
   OrderSummaryScreen(
       orderUiState = uiState,
       onCancelButtonClicked = {},
       onSendButtonClicked = { subject: String, summary: String ->

       }
   )
}

Bạn hiện có thể điều hướng qua từng màn hình của ứng dụng. Lưu ý khi gọi navigate(), màn hình không chỉ thay đổi mà còn được đặt ở đầu ngăn xếp lui. Ngoài ra, khi nhấn vào nút quay lại trên hệ thống, bạn có thể quay lại màn hình trước đó.

Ứng dụng xếp chồng từng màn hình lên trên màn hình trước đó và nút quay lại (bade5f3ecb71e4a2.png) có thể xoá những màn hình trên. Quá trình của màn hình từ startDestination dưới cùng đến màn hình trên cùng vừa xuất hiện được gọi là ngăn xếp lui.

Chuyển đến màn hình bắt đầu

Không giống như nút quay lại của hệ thống, nút Cancel (Huỷ) không quay lại màn hình trước đó. Thay vào đó, thao tác này sẽ bật lên, loại bỏ tất cả màn hình khỏi ngăn xếp lui và quay lại màn hình bắt đầu.

Bạn có thể thực hiện việc này bằng cách gọi phương thức popBackStack().

2f382e5eb319b4b8.png

Phương thức popBackStack() có hai tham số bắt buộc.

  • route: Chuỗi biểu thị tuyến của đích đến mà bạn muốn quay lại.
  • inclusive: Một giá trị Boolean mà nếu đúng thì giá trị này sẽ bật lên (xoá) tuyến được chỉ định. Nếu giá trị sai thì popBackStack() sẽ loại bỏ tất cả đích đến ở trên cùng nhưng không bao gồm đích đến bắt đầu, để đích đến này trở thành màn hình trên cùng mà người dùng thấy được.

Khi người dùng nhấn nút Cancel (Huỷ) trên bất kỳ màn hình nào, ứng dụng sẽ đặt lại trạng thái trong mô hình chế độ xem và gọi popBackStack(). Trước tiên, bạn sẽ triển khai một phương thức để thực hiện việc này, sau đó truyền phương thức này vào tham số thích hợp trên cả ba màn hình bằng nút Cancel (Huỷ).

  1. Sau hàm CupcakeApp(), hãy xác định một hàm riêng có tên là cancelOrderAndNavigateToStart().
private fun cancelOrderAndNavigateToStart() {
}
  1. Thêm hai tham số: viewModel thuộc loại OrderViewModelnavController thuộc loại NavHostController.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
}
  1. Trong phần nội dung hàm, hãy gọi resetOrder() trên viewModel.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
}
  1. Gọi popBackStack() trên navController, truyền vào CupcakeScreen.Start.name cho routefalse cho inclusive.
private fun cancelOrderAndNavigateToStart(
    viewModel: OrderViewModel,
    navController: NavHostController
) {
    viewModel.resetOrder()
    navController.popBackStack(CupcakeScreen.Start.name, inclusive = false)
}
  1. Trong thành phần kết hợp CupcakeApp(), truyền cancelOrderAndNavigateToStart vào tham số onCancelButtonClicked của hai thành phần kết hợp SelectOptionScreenOrderSummaryScreen.
composable(route = CupcakeScreen.Start.name) {
   StartOrderScreen(
       quantityOptions = quantityOptions,
       onNextButtonClicked = {
           viewModel.setQuantity(it)
           navController.navigate(CupcakeScreen.Flavor.name)
       }
   )
}
composable(route = CupcakeScreen.Flavor.name) {
   val context = LocalContext.current
   SelectOptionScreen(
       subtotal = uiState.price,
       onNextButtonClicked = { navController.navigate(CupcakeScreen.Pickup.name) },
       onCancelButtonClicked = {
           cancelOrderAndNavigateToStart(viewModel, navController)
       },
       options = flavors.map { id -> context.resources.getString(id) },
       onSelectionChanged = { viewModel.setFlavor(it) }
   )
}
  1. Chạy ứng dụng và kiểm thử để đảm bảo việc nhấn nút Cancel (Huỷ) trên mọi màn hình cũng sẽ điều hướng người dùng quay lại màn hình đầu tiên.

6. Chuyển đến ứng dụng khác

Cho đến hiện tại, bạn đã học cách điều hướng đến một màn hình khác trong ứng dụng và cách quay lại màn hình gốc. Chỉ còn một bước nữa thôi là bạn có thể triển khai tính năng điều hướng trong ứng dụng Cupcake. Trên màn hình tóm tắt đơn đặt hàng, người dùng có thể gửi đơn đặt hàng của mình đến một ứng dụng khác. Lựa chọn này sẽ hiện bảng dưới cùng (một thành phần giao diện người dùng che phủ phần dưới cùng của màn hình) cho thấy các lựa chọn chia sẻ.

Phần giao diện người dùng này không thuộc ứng dụng Cupcake. Trên thực tế, nó thuộc giao diện của hệ điều hành Android. Giao diện người dùng hệ thống, chẳng hạn như màn hình chia sẻ, không được gọi bởi navController của bạn. Thay vào đó, bạn sẽ dùng một tham số có tên là Ý định.

Ý định là một yêu cầu để hệ thống thực hiện một số hành động, thường là một hoạt động mới. Có nhiều ý định khác nhau và bạn nên tham khảo tài liệu để biết danh sách các ý định đầy đủ. Tuy nhiên, chúng ta quan tâm đến một ý định gọi là ACTION_SEND. Bạn có thể cung cấp ý định này bằng một số dữ liệu, chẳng hạn như một chuỗi và trình bày các thao tác chia sẻ phù hợp cho dữ liệu đó.

Quy trình thiết lập một ý định cơ bản như sau:

  1. Tạo một đối tượng có ý định và chỉ định ý định đó, chẳng hạn như ACTION_SEND.
  2. Chỉ định loại dữ liệu bổ sung đang được gửi cùng với ý định. Đối với một đoạn văn bản đơn giản, bạn có thể sử dụng "text/plain". Tuy nhiên, bạn cũng có thể dùng các loại văn bản có sẵn khác, chẳng hạn như "image/*" hoặc "video/*".
  3. Truyền mọi dữ liệu bổ sung vào ý định, chẳng hạn như văn bản hoặc hình ảnh cần chia sẻ, bằng cách gọi phương thức putExtra(). Ý định này sẽ có hai phần bổ sung là EXTRA_SUBJECTEXTRA_TEXT.
  4. Gọi phương thức startActivity() trong ngữ cảnh, truyền một hoạt động được tạo từ ý định.

Chúng tôi sẽ hướng dẫn bạn cách tạo ý định của hành động chia sẻ, nhưng quy trình này cũng giống như đối với các loại ý định khác. Đối với các dự án trong tương lai, bạn nên tham khảo tài liệu nếu cần để biết loại dữ liệu cụ thể và các dữ liệu bổ sung cần thiết.

Hoàn thành các bước sau để tạo ý định gửi đơn đặt hàng bánh nướng đến một ứng dụng khác:

  1. Trong CupcakeScreen.kt, bên dưới thành phần kết hợp CupcakeApp, hãy tạo một hàm riêng có tên là shareOrder().
private fun shareOrder()
  1. Thêm tham số có tên là context thuộc loại Context.
private fun shareOrder(context: Context) {
}
  1. Thêm hai tham số Stringsubjectsummary. Các chuỗi này sẽ hiển thị trên trang hành động chia sẻ.
private fun shareOrder(context: Context, subject: String, summary: String) {
}
  1. Trong phần nội dung của hàm, hãy tạo một Ý định có tên intent và truyền Intent.ACTION_SEND dưới dạng đối số.
val intent = Intent(Intent.ACTION_SEND)

Vì chỉ cần định cấu hình cho đối tượng Intent một lần, nên bạn có thể rút gọn các dòng mã tiếp theo bằng hàm apply() đã tìm hiểu trong lớp học lập trình trước đó.

  1. Gọi apply() trên Ý định mới được tạo và truyền vào một biểu thức lambda.
val intent = Intent(Intent.ACTION_SEND).apply {

}
  1. Trong nội dung hàm lambda, hãy đặt loại thành "text/plain". Vì đang thực hiện thao tác này trong một hàm được truyền vào apply(), nên bạn không cần tham chiếu đến giá trị nhận dạng của đối tượng là intent.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
}
  1. Gọi putExtra(), truyền tiêu đề cho EXTRA_SUBJECT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
}
  1. Gọi putExtra(), truyền tóm tắt cho EXTRA_TEXT.
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_SUBJECT, subject)
    putExtra(Intent.EXTRA_TEXT, summary)
}
  1. Gọi phương thức ngữ cảnh startActivity().
context.startActivity(

)
  1. Trong lambda được truyền vào startActivity(), hãy tạo một hoạt động từ Ý định bằng cách gọi phương thức lớp createChooser(). Truyền ý định cho đối số đầu tiên và tài nguyên chuỗi new_cupcake_order.
context.startActivity(
    Intent.createChooser(
        intent,
        context.getString(R.string.new_cupcake_order)
    )
)
  1. Tại thành phần kết hợp CupcakeApp, trong lệnh gọi đến composable() cho CucpakeScreen.Summary.name, lấy thông tin tham chiếu đến đối tượng ngữ cảnh để bạn có thể truyền cho hàm shareOrder().
composable(route = CupcakeScreen.Summary.name) {
    val context = LocalContext.current

    ...
}
  1. Trong phần thân hàm lambda của onSendButtonClicked(), gọi shareOrder(), truyền vào context, subjectsummary làm đối số.
onSendButtonClicked = { subject: String, summary: String ->
    shareOrder(context, subject = subject, summary = summary)
}
  1. Chạy ứng dụng và điều hướng qua các màn hình.

Khi nhấp vào Send Order to Another App (Gửi đơn đặt hàng đến một ứng dụng khác), bạn sẽ thấy thao tác chia sẻ như Messaging (Nhắn tin) và Bluetooth ở bảng dưới cùng, cùng với tiêu đề và phần tóm tắt mà bạn đã cung cấp dưới dạng bổ sung.

Ứng dụng Cupcake cho người dùng thấy các lựa chọn chia sẻ như tin nhắn SMS hoặc Email.

7. Làm cho AppBar (thanh ứng dụng) phản hồi tính năng điều hướng

Mặc dù ứng dụng đã hoạt động cũng như có thể điều hướng đi và đến từ mọi màn hình, nhiều nội dung trong ảnh chụp màn hình ở đầu lớp học lập trình vẫn còn chưa được nhắc đến. AppBar (thanh ứng dụng) không tự động phản hồi chức năng điều hướng. Tiêu đề không tự cập nhật mỗi khi ứng dụng điều hướng đến một tuyến mới, cũng như không hiện nút Mũi tên lên trước tiêu đề khi thích hợp.

Mã khởi động chứa một thành phần kết hợp để quản lý AppBar có tên là CupcakeAppBar. Bây giờ, sau khi đã triển khai tính năng điều hướng trong ứng dụng, bạn có thể sử dụng thông tin từ ngăn xếp lui để hiển thị tiêu đề chính xác và hiện nút Mũi tên lên khi thích hợp.

Nút Mũi tên lên chỉ hiện khi có một thành phần kết hợp trong ngăn xếp lui. Nếu ứng dụng không có màn hình nào trong ngăn xếp lui (StartOrderScreen xuất hiện) thì nút Mũi tên lên sẽ không xuất hiện. Để kiểm tra điều này, bạn cần tham chiếu đến ngăn xếp lui.

  1. Trong thành phần kết hợp CupcakeApp, bên dưới biến navController, tạo một biến có tên backStackEntry và gọi phương thức currentBackStackEntry() của navController bằng cách sử dụng uỷ quyền by.
@Composable
fun CupcakeApp(modifier: Modifier = Modifier, viewModel: OrderViewModel = viewModel()){

    val navController = rememberNavController()

    val backStackEntry by navController.currentBackStackEntryAsState()

    ...
}
  1. Trong CupcakeAppBar, hãy truyền backStackEntry?.destination?.route cho tham số currentScreen. Vì giá trị này có thể rỗng, dùng toán tử elvis (?:) để chỉ định CupcakeScreen.Start.name làm giá trị mặc định.
currentScreen = backStackEntry?.destination?.route ?: CupcakeScreen.Start.name,

Miễn là có một màn hình phía sau màn hình hiện tại trong ngăn xếp lui thì nút Mũi tên lên sẽ xuất hiện. Bạn có thể sử dụng biểu thức boolean để xác định xem nút Mũi tên lên có xuất hiện hay không.

  1. Đối với tham số canNavigateBack, truyền biểu thức boolean để kiểm tra xem thuộc tính previousBackStackEntry của navController có bằng với giá trị rỗng hay không.
canNavigateBack = navController.previousBackStackEntry != null,
  1. Gọi phương thức navigateUp() của navController để quay lại màn hình trước đó.
navigateUp = { navController.navigateUp() }
  1. Chạy ứng dụng của bạn.

Lưu ý rằng tiêu đề AppBar hiện đã cập nhật để phản ánh màn hình hiện tại. Khi di chuyển đến một màn hình khác màn hình StartOrderScreen, nút Mũi tên lên sẽ xuất hiện và đưa bạn về màn hình trước.

Ảnh động minh hoạ quá trình người dùng di chuyển qua các màn hình trong ứng dụng Cupcake đã hoàn thiện.

8. Lấy mã giải pháp

Để tải xuống mã cho lớp học lập trình đã kết thúc, bạn có thể sử dụng lệnh git này:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-cupcake.git

$ cd basic-android-kotlin-compose-training-cupcake
$ git checkout navigation

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp ZIP, sau đó giải nén và mở trong Android Studio.

Nếu bạn muốn tham khảo mã giải pháp cho lớp học lập trình này, hãy xem trên GitHub.

9. Tóm tắt

Xin chúc mừng! Bạn vừa chuyển từ ứng dụng có một màn hình đơn giản sang ứng dụng phức tạp với nhiều màn hình bằng cách dùng thành phần Điều hướng trong Jetpack để di chuyển qua nhiều màn hình. Bạn đã xác định tuyến, xử lý các tuyến đó trong NavHost và sử dụng tham số loại hàm để tách logic điều hướng khỏi các màn hình riêng lẻ. Bạn cũng đã tìm hiểu cách gửi dữ liệu đến một ứng dụng khác bằng ý định cũng như tuỳ chỉnh thanh ứng dụng để phản hồi thao tác điều hướng. Trong các bài sau, bạn sẽ tiếp tục sử dụng những kỹ năng này khi làm việc trên một số ứng dụng nhiều màn hình khác với độ phức tạp ngày càng tăng.

Tìm hiểu thêm