Giới thiệu về trạng thái trong Compose

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

Lớp học lập trình này hướng dẫn bạn về trạng thái (state) và cách có thể sử dụng và thao tác trạng thái trong Jetpack Compose.

Về cơ bản, trạng thái trong một ứng dụng là bất kỳ giá trị nào có thể thay đổi theo thời gian. Định nghĩa này rất rộng và bao gồm mọi thứ từ cơ sở dữ liệu đến biến trong ứng dụng. Bạn sẽ tìm hiểu thêm về cơ sở dữ liệu trong bài học sau, nhưng bây giờ bạn chỉ cần biết rằng cơ sở dữ liệu là một tập hợp được sắp xếp của thông tin có cấu trúc, chẳng hạn như tệp trên máy tính.

Tất cả ứng dụng Android đều cho người dùng thấy trạng thái. Sau đây là một số ví dụ về trạng thái trong ứng dụng Android:

  • Một thông báo xuất hiện khi không thể thiết lập kết nối mạng.
  • Các biểu mẫu, chẳng hạn như biểu mẫu đăng ký. Trạng thái có thể là: đã điền và đã gửi.
  • Thành phần điều khiển có thể nhấn, chẳng hạn như nút. Trạng thái có thể là chưa nhấn, đang nhấn (hiển thị ảnh động) hoặc đã nhấn (một hành động onClick).

Trong lớp học lập trình này, bạn sẽ tìm hiểu cách sử dụng và hoạt động của trạng thái khi dùng Compose. Để làm vậy, hãy tạo một ứng dụng tính tiền boa có tên là Tip Time (Tính tiền boa) chứa các phần tử trên giao diện người dùng mà Compose đã tích hợp sẵn:

  • Một thành phần kết hợp TextField để nhập và chỉnh sửa văn bản.
  • Một thành phần kết hợp Text để hiện văn bản.
  • Một thành phần kết hợp Spacer để hiện không gian trống giữa các phần tử trên giao diện người dùng.

Kết thúc lớp học lập trình này, bạn sẽ tạo được một công cụ tính tiền boa có tính tương tác. Công cụ này sẽ tự động tính số tiền boa khi bạn nhập số tiền dịch vụ. Hình ảnh dưới đây cho thấy giao diện của ứng dụng hoàn thiện:

d6c6ed627ffa4.png.

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

  • Hiểu biết cơ bản về Compose, chẳng hạn như chú thích @Composable.
  • Quen thuộc cơ bản với bố cục Compose, chẳng hạn như thành phần kết hợp bố cục RowColumn.
  • Quen thuộc cơ bản với phương thức sửa đổi (modifier), chẳng hạn như hàm Modifier.padding().
  • Quen thuộc với thành phần kết hợp Text.

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

  • Cách suy nghĩ về trạng thái trong giao diện người dùng.
  • Cách Compose sử dụng trạng thái để hiển thị dữ liệu.
  • Cách thêm hộp văn bản vào ứng dụng.
  • Cách chuyển trạng thái lên trên.

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

  • Ứng dụng tính tiền boa có tên là Tip Time để tính số tiền boa dựa trên số tiền dịch vụ.

Bạn cần có

  • Máy tính có kết nối Internet và trình duyệt web
  • Kiến thức về Kotlin
  • Phiên bản mới nhất của Android Studio

2. Bắt đầu

  1. Xem Công cụ tính tiền boa trực tuyến của Google. Xin lưu ý đây chỉ là một ví dụ, không phải là ứng dụng Android mà bạn sẽ tạo trong khoá học này.

b7d1ae0f60c4ba2e.png 19b877bbeca9ef9.png

  1. Nhập các giá trị khác nhau vào hộp Bill (Hoá đơn) và Tip % (% Tiền boa). Giá trị tiền boa và tổng giá trị thay đổi.

c793ff18ad2060e9.png

Lưu ý tại thời điểm bạn nhập giá trị, Tip (Tiền boa) và Total (Tổng giá trị) được cập nhật. Khi kết thúc khóa học lập trình sau đây, bạn sẽ phát triển ứng dụng tính tiền boa tương tự trong Android.

Trong lộ trình này, bạn sẽ tạo một ứng dụng Android tính tiền boa đơn giản.

Các nhà phát triển thường sẽ thực hiện theo cách này – tạo ra một phiên bản ứng dụng đơn giản nhưng hoạt động được (ngay cả khi ứng dụng trông không đẹp mắt) rồi thêm các tính năng khác và chỉnh sửa lại giao diện sau.

Khi bạn kết thúc lớp học lập trình này, ứng dụng tính tiền boa của bạn sẽ trông giống như các ảnh chụp màn hình dưới đây. Khi người dùng nhập số tiền trên hoá đơn, ứng dụng của bạn sẽ hiện đề xuất về số tiền boa. Hiện tại, tỷ lệ phần trăm tiền boa được cố định giá trị trong mã là 15%. Trong lớp học lập trình tiếp theo, bạn sẽ tiếp tục làm việc với ứng dụng của mình và thêm các tính năng như đặt tỷ lệ phần trăm tiền boa tùy chỉnh.

3. Lấy mã khởi đầu

Mã khởi đầu là mã được viết sẵn, có thể dùng làm điểm bắt đầu cho dự án mới. Mã đó cũng có thể giúp bạn tập trung vào các khái niệm mới được dạy trong lớp học lập trình này.

Bắt đầu với mã khởi đầu bằng cách tải mã đó xuống tại đây:

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-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout starter

Bạn có thể duyệt qua mã khởi đầu trong kho lưu trữ GitHub TipTime.

Tổng quan về ứng dụng khởi đầu

Để làm quen với mã khởi đầu, hãy hoàn thành các bước sau đây:

  1. Mở dự án bằng mã khởi đầu trong Android Studio.
  2. Chạy ứng dụng trên thiết bị Android hoặc trình mô phỏng.
  3. Bạn sẽ thấy hai thành phần văn bản: một dành cho nhãn và thành phần còn lại dùng để hiện số tiền boa.

78e9ba2ba645b19e.png.

Tìm hiểu đoạn mã khởi đầu

Mã khởi đầu có các thành phần kết hợp văn bản. Trong lộ trình này, bạn sẽ thêm trường văn bản để nhận thông tin đầu vào của người dùng. Dưới đây là hướng dẫn từng bước ngắn gọn về một số tệp để bạn bắt đầu.

res > values > strings.xml

<resources>
   <string name="app_name">Tip Time</string>
   <string name="calculate_tip">Calculate Tip</string>
   <string name="bill_amount">Bill Amount</string>
   <string name="tip_amount">Tip Amount: %s</string>
</resources>

Đây là tệp string.xml trong những tài nguyên có tất cả các chuỗi mà bạn sẽ dùng trong ứng dụng này.

MainActivity

Tệp này chủ yếu chứa mã được tạo theo mẫu và các hàm sau đây.

  • Hàm TipTimeLayout() chứa một phần tử Column có hai thành phần kết hợp văn bản mà bạn thấy trong các ảnh chụp màn hình. Hàm này cũng có thành phần kết hợp spacer để thêm không gian nhằm tăng tính thẩm mỹ.
  • Hàm calculateTip() nhận số tiền trên hoá đơn và tính số tiền boa bằng 15%. Tham số tipPercent được đặt thành giá trị đối số mặc định 15.0. Thao tác này sẽ đặt giá trị tiền boa mặc định hiện tại là 15%. Trong lớp học lập trình tiếp theo, bạn sẽ nhận giá trị tiền boa từ người dùng.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = stringResource(R.string.calculate_tip),
            modifier = Modifier
                .padding(bottom = 16.dp, top = 40.dp)
                .align(alignment = Alignment.Start)
        )
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        Spacer(modifier = Modifier.height(150.dp))
    }
}
private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
   val tip = tipPercent / 100 * amount
   return NumberFormat.getCurrencyInstance().format(tip)
}

Trong khối Surface() của hàm onCreate(), hàm TipTimeLayout() đang được gọi. Thao tác này sẽ làm hiện bố cục của ứng dụng trong thiết bị hoặc trình mô phỏng.

override fun onCreate(savedInstanceState: Bundle?) {
   //...
   setContent {
       TipTimeTheme {
           Surface(
           //...
           ) {
               TipTimeLayout()
           }
       }
   }
}

Trong khối TipTimeTheme của hàm TipTimeLayoutPreview(), hàm TipTimeLayout() đang được gọi. Thao tác này sẽ làm hiện bố cục của ứng dụng trong phần Design (Thiết kế) và trong ngăn Split (Phân tách).

@Preview(showBackground = true)
@Composable
fun TipTimeLayoutPreview() {
   TipTimeTheme {
       TipTimeLayout()
   }
}

83ddead6f1179fbc.png

Lấy thông tin đầu vào từ người dùng

Trong phần này, bạn thêm phần tử trên giao diện người dùng cho phép người dùng nhập số tiền trên hoá đơn vào ứng dụng. Bạn có thể xem giao diện của phần này trong ảnh dưới đây:

cc51b428369a893d.png

Ứng dụng của bạn dùng một kiểu và giao diện tuỳ chỉnh.

Kiểu và giao diện là tập hợp các thuộc tính chỉ định giao diện của một phần tử trên giao diện người dùng. Kiểu có thể chỉ định các thuộc tính như màu phông chữ, cỡ chữ, màu nền và nhiều thuộc tính khác có thể áp dụng cho toàn bộ ứng dụng. Các lớp học lập trình sau này sẽ đề cập đến cách triển khai những thuộc tính kể trên trong ứng dụng của bạn. Hiện tại, chúng tôi đã thực hiện điều này nhằm giúp ứng dụng của bạn trông bắt mắt hơn.

Để giúp bạn hiểu rõ hơn, dưới đây là bảng so sánh song song phiên bản giải pháp của ứng dụng khi có và không có giao diện tuỳ chỉnh.

Khi không có giao diện tuỳ chỉnh.

Khi có giao diện tuỳ chỉnh.

Hàm TextField có khả năng kết hợp cho phép người dùng nhập văn bản vào ứng dụng. Ví dụ: hãy chú ý đến hộp văn bản trên màn hình đăng nhập của ứng dụng Gmail trong ảnh dưới đây:

Màn hình điện thoại có ứng dụng Gmail chứa trường văn bản cho email

Thêm thành phần kết hợp TextField vào ứng dụng:

  1. Trong tệp MainActivity.kt, hãy thêm một hàm EditNumberField() có khả năng kết hợp. Hàm này sẽ lấy tham số Modifier.
  2. Trong phần nội dung của hàm EditNumberField() bên dưới TipTimeLayout(), hãy thêm TextField chấp nhận một tham số có tên value được đặt thành chuỗi trống và một tham số có tên onValueChange được đặt thành biểu thức lambda trống:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   TextField(
      value = "",
      onValueChange = {},
      modifier = modifier
   )
}
  1. Hãy chú ý các tham số mà bạn truyền:
  • Tham số value là hộp văn bản hiện giá trị chuỗi mà bạn truyền vào đây.
  • Tham số onValueChange là lệnh gọi lại lambda được kích hoạt khi người dùng nhập văn bản vào hộp văn bản.
  1. Nhập hàm dưới đây:
import androidx.compose.material3.TextField
  1. Trong thành phần kết hợp TipTimeLayout(), trên dòng sau hàm đầu tiên có khả năng kết hợp văn bản, hãy gọi hàm EditNumberField(), truyền đối tượng sửa đổi sau đây.
import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun TipTimeLayout() {
   Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
   ) {
       Text(
           ...
       )
       EditNumberField(modifier = Modifier.padding(bottom = 32.dp).fillMaxWidth())
       Text(
           ...
       )
       ...
   }
}

Thao tác này sẽ làm hiện hộp văn bản trên màn hình.

  1. Trong ngăn Design (Thiết kế), bạn sẽ thấy văn bản Calculate Tip, một hộp văn bản trống và thành phần kết hợp văn bản Tip Amount.

2f2ef25c956e357f.png

4. Dùng trạng thái trong Compose

Trạng thái trong ứng dụng là giá trị bất kỳ có thể thay đổi theo thời gian. Trong ứng dụng này, trạng thái là số tiền trên hoá đơn.

Thêm biến vào trạng thái lưu trữ:

  1. Ở đầu hàm EditNumberField(), dùng từ khoá val để thêm biến amountInput, đặt biến này thành giá trị "0":
val amountInput = "0"

Đây là trạng thái của ứng dụng cho số tiền trên hoá đơn.

  1. Đặt tham số có tên value thành giá trị amountInput:
TextField(
   value = amountInput,
   onValueChange = {},
)
  1. Kiểm tra bản xem trước. Hộp văn bản hiện giá trị được đặt thành biến trạng thái như bạn thấy trong hình ảnh dưới đây:

ecbf5f5015668e.png

  1. Khi chạy ứng dụng trong trình mô phỏng, hãy nhập một giá trị khác. Trạng thái được cố định giá trị trong mã vẫn không thay đổi vì thành phần kết hợp TextField không tự cập nhật. Thành phần này cập nhật khi tham số value thay đổi và được đặt thành thuộc tính amountInput.

Biến amountInput biểu thị cho trạng thái của hộp văn bản. Trạng thái được cố định giá trị trong mã không hữu ích vì không thể sửa đổi và không phản ánh hoạt động đầu vào của người dùng. Bạn cần cập nhật trạng thái của ứng dụng khi người dùng cập nhật số tiền trên hoá đơn.

5. Thành phần kết hợp

Thành phần kết hợp trong ứng dụng mô tả giao diện người dùng cho thấy cột có một số văn bản, một dấu cách và một hộp văn bản. Văn bản cho thấy văn bản Calculate Tip và hộp văn bản hiện giá trị 0 hoặc giá trị mặc định bất kỳ.

Compose là khung giao diện người dùng khai báo, có nghĩa là bạn khai báo giao diện người dùng sẽ trông như thế nào trong mã. Nếu muốn hộp văn bản hiển thị giá trị 100 ban đầu, bạn nên đặt giá trị ban đầu trong mã cho các thành phần kết hợp là giá trị 100.

Điều gì xảy ra nếu bạn muốn giao diện người dùng thay đổi trong khi ứng dụng đang chạy hoặc khi người dùng tương tác với ứng dụng? Ví dụ: điều gì xảy ra nếu bạn muốn cập nhật biến amountInput bằng giá trị do người dùng nhập và hiển thị trong hộp văn bản? Đó là khi bạn dựa vào một quy trình có tên là kết hợp lại (recomposition) để cập nhật Thành phần kết hợp của ứng dụng.

Thành phần kết hợp là nội dung mô tả về giao diện người dùng do Compose tạo ra khi thực thi các thành phần kết hợp. Các ứng dụng Compose gọi các hàm có khả năng kết hợp để biến đổi dữ liệu thành giao diện người dùng. Nếu có thay đổi về trạng thái, thì Compose sẽ thực thi lại các hàm có khả năng kết hợp bị ảnh hưởng với trạng thái mới. Việc này sẽ tạo ra một giao diện người dùng cập nhật – đây được gọi là kết hợp lại. Compose lên lịch kết hợp lại cho bạn.

Khi Compose chạy các thành phần kết hợp lần đầu tiên trong quá trình kết hợp ban đầu, bộ công cụ này sẽ theo dõi các thành phần kết hợp bạn gọi để mô tả Giao diện người dùng trong một thành phần kết hợp. Quá trình kết hợp lại xảy ra khi Compose thực thi lại các thành phần kết hợp có thể đã thay đổi theo thay đổi về trạng thái và sau đó cập nhật thành phần kết hợp để phản ánh mọi thay đổi.

Thành phần kết hợp chỉ có thể được quá trình kết hợp ban đầu tạo ra và cập nhật bằng quá trình kết hợp lại. Phương pháp duy nhất để chỉnh sửa thành phần kết hợp là kết hợp lại. Để làm điều này, Compose cần biết trạng thái cần theo dõi để có thể lên lịch kết hợp lại khi nhận được lệnh cập nhật. Trong trường hợp của bạn, đó là biến amountInput, vì vậy bất cứ khi nào giá trị của biến này thay đổi, Compose sẽ lên lịch kết hợp lại.

Bạn sử dụng loại StateMutableState trong Compose để làm cho Compose có thể quan sát hoặc theo dõi trạng thái trong ứng dụng. Loại State là không thể thay đổi, vì vậy bạn chỉ có thể đọc giá trị trong loại đó, trong khi loại MutableState có thể thay đổi. Bạn có thể dùng hàm mutableStateOf() để tạo một MutableState có thể quan sát được. Hàm này nhận giá trị ban đầu dưới dạng tham số được gói trong đối tượng State, sau đó khiến value có thể quan sát được.

Giá trị được hàm mutableStateOf() trả về:

  • Duy trì trạng thái, tức số tiền trên hoá đơn.
  • Có thể thay đổi, vì vậy giá trị có thể thay đổi.
  • Có thể quan sát được, vì vậy Compose quan sát mọi thay đổi về giá trị và kích hoạt quy trình kết hợp lại để cập nhật giao diện người dùng.

Thêm trạng thái chi phí dịch vụ:

  1. Trong hàm EditNumberField(), thay đổi từ khoá val trước biến trạng thái amountInput thành từ khoá var:
var amountInput = "0"

Thao tác này có thể thay đổi amountInput.

  1. Dùng loại MutableState<String> thay vì biến String được cố định giá trị trong mã để Compose biết cần theo dõi trạng thái amountInput và sau đó truyền vào chuỗi "0", vốn là giá trị mặc định ban đầu của biến trạng thái amountInput:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

var amountInput: MutableState<String> = mutableStateOf("0")

Quá trình khởi chạy amountInput cũng có thể được viết như sau theo thông tin suy luận kiểu.

var amountInput = mutableStateOf("0")

Hàm mutableStateOf() nhận giá trị "0" ban đầu làm đối số, sau đó khiến amountInput có thể quan sát được. Việc này sẽ dẫn đến cảnh báo biên dịch sau đây trong Android Studio, nhưng bạn sẽ sớm khắc phục được:

Creating a state object during composition without using remember.
  1. Trong hàm có khả năng kết hợp TextField, sử dụng thuộc tính amountInput.value:
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose theo dõi từng thành phần kết hợp đọc thuộc tính value của trạng thái và kích hoạt quá trình kết hợp lại khi value thay đổi.

Lệnh gọi lại onValueChange được kích hoạt khi giá trị nhập vào của hộp văn bản thay đổi. Trong biểu thức lambda, biến it chứa giá trị mới.

  1. Trong biểu thức lambda của tham số có tên onValueChange, đặt thuộc tính amountInput.value thành biến it:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput = mutableStateOf("0")
   TextField(
       value = amountInput.value,
       onValueChange = { amountInput.value = it },
       modifier = modifier
   )
}

Bạn đang cập nhật trạng thái của TextField (tức là biến amountInput), khi TextField thông báo cho bạn rằng có thay đổi trong văn bản thông qua hàm gọi lại onValueChange.

  1. Chạy ứng dụng và nhập văn bản vào hộp văn bản. Hộp văn bản vẫn hiện giá trị 0 như bạn thấy trong hình ảnh dưới đây:

3a2c62f8ec55e339.gif

Khi người dùng nhập văn bản vào hộp văn bản, lệnh gọi lại onValueChange được gọi và biến amountInput được cập nhật với giá trị mới. Compose theo dõi trạng thái amountInput, do đó ngay khi giá trị của trạng thái này thay đổi, quá trình kết hợp lại sẽ được lên lịch và hàm có khả năng kết hợpEditNumberField() sẽ được thực thi lại. Trong hàm có khả năng kết hợp đó, biến amountInput được đặt lại về giá trị 0 ban đầu. Do đó, hộp văn bản hiển thị giá trị 0.

Sau khi bạn thêm mã, thay đổi trạng thái sẽ khiến quá trình kết hợp lại được lên lịch.

Tuy nhiên, bạn cần một cách để lưu giữ giá trị của biến amountInput trong quá trình kết hợp lại để biến này không được đặt lại thành giá trị 0 mỗi khi hàm EditNumberField() kết hợp lại. Bạn sẽ giải quyết vấn đề này trong phần tiếp theo.

6. Dùng hàm nhớ để lưu trạng thái

Các phương thức thành phần kết hợp có thể được gọi nhiều lần do quá trình kết hợp lại. Thành phần kết hợp đặt lại trạng thái trong quá trình kết hợp lại nếu không được lưu.

Các hàm có khả năng kết hợp có thể lưu trữ đối tượng trong quá trình kết hợp lại bằng remember. Một giá trị do hàm remember tính toán được lưu trữ trong Thành phần kết hợp trong quá trình kết hợp ban đầu và giá trị đã lưu trữ được trả về trong quá trình kết hợp lại. Thông thường, hàm remembermutableStateOf được dùng cùng nhau trong hàm có khả năng kết hợp để trạng thái và nội dung cập nhật của hàm được phản ánh chính xác trong giao diện người dùng.

Dùng hàm remember trong hàm EditNumberField():

  1. Trong hàm EditNumberField(), khởi tạo biến amountInput có uỷ quyền thuộc tính Kotlin by remember, bằng cách bao quanh lệnh gọi đến hàm mutableStateOf() bằng remember.
  2. Trong hàm mutableStateOf(), truyền vào một chuỗi trống thay vì một chuỗi "0" tĩnh:
var amountInput by remember { mutableStateOf("") }

Bây giờ, chuỗi trống là giá trị mặc định ban đầu cho biến amountInput. by là một Ủy quyền thuộc tính Kotlin. Các hàm getter và setter mặc định cho thuộc tính amountInput được ủy quyền tương ứng với các hàm getter và setter của lớp remember.

  1. Nhập các hàm dưới đây:
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Việc thêm lệnh nhập getter và setter của phần tử uỷ quyền cho phép bạn đọc và đặt amountInput mà không cần tham chiếu đến thuộc tính value của MutableState.

Hàm EditNumberField() được cập nhật sẽ có dạng như sau:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }
   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       modifier = modifier
   )
}
  1. Chạy ứng dụng và nhập một số văn bản vào hộp văn bản. Bạn sẽ nhìn thấy văn bản mình vừa nhập.

f60dddc9dcf03edf.png.

7. Trạng thái và thành phần kết hợp lại trong thực tế

Trong phần này, bạn đặt một điểm ngắt và gỡ lỗi hàm có khả năng kết hợp EditNumberField() để xem quá trình kết hợp và kết hợp lại hoạt động như thế nào.

Đặt điểm ngắt và gỡ lỗi ứng dụng trên trình mô phỏng hoặc thiết bị:

  1. Trong hàm EditNumberField() bên cạnh tham số có tên onValueChange, đặt điểm ngắt dòng.
  2. Trong trình đơn điều hướng, nhấp vào Gỡ lỗi "ứng dụng". Ứng dụng sẽ chạy trên trình mô phỏng hoặc thiết bị. Quá trình thực thi của ứng dụng sẽ tạm dừng lần đầu tiên khi phần tử TextField được tạo.

e2e1541f22e39281.png

  1. Trong ngăn Debug (Gỡ lỗi), hãy nhấp vào 7bdc150b4ddfdab3.png Resume Program (Tiếp tục chương trình). Hộp văn bản đã được tạo.
  2. Trên trình mô phỏng hoặc thiết bị, hãy nhập một chữ cái vào hộp văn bản. Quá trình thực thi của ứng dụng sẽ tạm dừng lần nữa khi đến điểm ngắt mà bạn đã đặt.

Khi bạn nhập văn bản, lệnh gọi lại onValueChange sẽ được gọi. Bên trong lambda it có giá trị mới mà bạn đã nhập vào bàn phím.

Sau khi giá trị "it" được gán cho amountInput, Compose sẽ kích hoạt quá trình kết hợp lại với dữ liệu mới khi giá trị quan sát được đã thay đổi.

987b5951f9f33262.png

  1. Trong ngăn Debug (Gỡ lỗi), hãy nhấp vào 7bdc150b4ddfdab3.png Resume Program (Tiếp tục chương trình). Có thể thấy văn bản được nhập vào trình mô phỏng hoặc trên thiết bị xuất hiện bên cạnh dòng có điểm ngắt trong hình ảnh dưới đây:

7e7a3c1a4a64e987.png

Đây là trạng thái của trường văn bản.

  1. Nhấp vào 7bdc150b4ddfdab3.png Resume Program (Tiếp tục chương trình). Giá trị đã nhập sẽ xuất hiện trên trình mô phỏng hoặc thiết bị.

8. Sửa đổi giao diện

Trong phần trước, bạn đã làm cho trường văn bản hoạt động. Trong phần này, bạn sẽ cải thiện giao diện người dùng.

Thêm nhãn vào hộp văn bản

Mỗi hộp văn bản phải có một nhãn cho phép người dùng biết họ có thể nhập thông tin nào. Trong phần đầu tiên của hình ảnh ví dụ sau, văn bản nhãn nằm ở giữa một trường văn bản và được căn chỉnh với dòng đầu vào. Trong phần thứ hai của hình ảnh ví dụ sau, nhãn được di chuyển lên cao hơn trong hộp văn bản khi người dùng nhấp vào hộp văn bản để nhập văn bản. Để tìm hiểu thêm về kết cấu trường văn bản, hãy xem Kết cấu.

9e802ed30b7612b0.png

Sửa đổi hàm EditNumberField() để thêm nhãn vào trường văn bản:

  1. Trong hàm có khả năng kết hợp TextField() của hàm EditNumberField(), thêm tham số có tên label được đặt thành biểu thức lambda trống:
TextField(
//...
   label = { }
)
  1. Trong biểu thức lambda, hãy gọi hàm Text() chấp nhận stringResource(R.string.bill_amount):
label = { Text(stringResource(R.string.bill_amount)) },
  1. Trong hàm TextField() có khả năng kết hợp, hãy thêm tham số có tên singleLine được đặt thành giá trị true:
TextField(
  // ...
   singleLine = true,
)

Thao tác này nén hộp văn bản thành một dòng có thể cuộn theo chiều ngang trong nhiều dòng.

  1. Thêm tập hợp tham số keyboardOptions vào KeyboardOptions():
import androidx.compose.foundation.text.KeyboardOptions

TextField(
  // ...
   keyboardOptions = KeyboardOptions(),
)

Android cung cấp một lựa chọn để định cấu hình bàn phím hiển thị trên màn hình để nhập chữ số, địa chỉ email, URL và mật khẩu, v.v. Để tìm hiểu thêm về các loại bàn phím khác, xem KeyboardType.

  1. Đặt loại bàn phím thành bàn phím số để nhập chữ số. Truyền hàm KeyboardOptions một tham số có tên keyboardType được đặt thành KeyboardType.Number:
import androidx.compose.ui.text.input.KeyboardType

TextField(
  // ...
   keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
)

Hàm EditNumberField() hoàn chỉnh sẽ có dạng như đoạn mã dưới đây:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
    var amountInput by remember { mutableStateOf("") }
    TextField(
        value = amountInput,
        onValueChange = { amountInput = it },
        singleLine = true,
        label = { Text(stringResource(R.string.bill_amount)) },
        keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
        modifier = modifier
    )
}
  1. Chạy ứng dụng.

Bạn có thể thấy những thay đổi về bàn phím trong ảnh chụp màn hình dưới đây:

bbd4c90747fb8d28.png

9. Hiện số tiền boa

Trong phần này, bạn sẽ triển khai chức năng chính của ứng dụng, đó là khả năng tính và hiện số tiền boa.

Trong tệp MainActivity.kt, hàm private calculateTip() được cung cấp cho bạn như một phần của mã khởi đầu. Bạn sẽ dùng hàm này để tính số tiền boa:

private fun calculateTip(amount: Double, tipPercent: Double = 15.0): String {
    val tip = tipPercent / 100 * amount
    return NumberFormat.getCurrencyInstance().format(tip)
}

Trong phương thức trên, bạn đang dùng NumberFormat để hiện tiền boa dưới định dạng đơn vị tiền tệ.

Bây giờ, ứng dụng của bạn có thể tính tiền boa, nhưng bạn vẫn cần định dạng và hiện số tiền boa bằng lớp.

Dùng hàm calculateTip()

Văn bản do người dùng nhập vào thành phần kết hợp trường văn bản được trả về hàm callback (gọi lại) onValueChange dưới dạng String mặc dù người dùng đã nhập một số. Để khắc phục lỗi này, bạn cần chuyển đổi giá trị amountInput, trong đó có số tiền mà người dùng nhập.

  1. Trong hàm EditNumberField() có khả năng kết hợp, hãy tạo một biến mới có tên là amount sau định nghĩa amountInput. Gọi hàm toDoubleOrNull trên biến amountInput để chuyển đổi String thành Double:
val amount = amountInput.toDoubleOrNull()

Hàm toDoubleOrNull() là một hàm Kotlin xác định trước có thể phân tích cú pháp chuỗi dưới dạng số Double và trả về kết quả hoặc null nếu chuỗi đó không phải là cách biểu diễn hợp lệ của một số.

  1. Ở cuối câu lệnh, thêm một toán tử Elvis ?: trả về giá trị 0.0 khi amountInput có giá trị rỗng (null):
val amount = amountInput.toDoubleOrNull() ?: 0.0
  1. Sau biến amount, tạo một biến val khác có tên là tip. Khởi chạy với calculateTip(), truyền tham số amount.
val tip = calculateTip(amount)

Hàm EditNumberField() sẽ trông giống như đoạn mã sau:

@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   TextField(
       value = amountInput,
       onValueChange = { amountInput = it },
       label = { Text(stringResource(R.string.bill_amount)) },
       modifier = Modifier.fillMaxWidth(),
       singleLine = true,
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
   )
}

Hiện số tiền boa đã tính

Bạn đã viết hàm để tính số tiền boa, bước tiếp theo là hiện số tiền boa đã tính:

  1. Trong hàm TipTimeLayout() ở cuối khối Column(), hãy chú ý đến thành phần kết hợp văn bản hiển thị $0.00. Bạn sẽ cập nhật giá trị này thành số tiền boa đã tính.
@Composable
fun TipTimeLayout() {
    Column(
        modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // ...
        Text(
            text = stringResource(R.string.tip_amount, "$0.00"),
            style = MaterialTheme.typography.displaySmall
        )
        // ...
    }
}

Bạn cần dùng biến amountInput trong hàm TipTimeLayout() để tính và hiện số tiền boa, nhưng biến amountInput là trạng thái của trường văn bản được xác định trong hàm EditNumberField() có khả năng kết hợp, do đó bạn chưa thể gọi biến này trong hàm TipTimeLayout(). Hình ảnh dưới đây minh hoạ cấu trúc của mã:

4d8b69d49a90683a.png

Cấu trúc này sẽ không cho phép bạn hiện số tiền boa trong thành phần kết hợp Text mới vì thành phần kết hợp Text cần dùng biến amount được tính từ biến amountInput. Bạn cần hiển thị biến amount cho hàm TipTimeLayout(). Hình ảnh dưới đây minh hoạ cấu trúc mã mong muốn, khiến thành phần kết hợp EditNumberField() không có trạng thái:

38bd92a2346a910b.png

Mẫu này được gọi là state hoisting (chuyển trạng thái lên trên). Trong phần tiếp theo, bạn chuyển lên trên hoặc nâng trạng thái từ một thành phần kết hợp để làm cho thành phần kết hợp đó không có trạng thái.

10. State hoisting (Chuyển trạng thái lên trên)

Trong phần này, bạn tìm hiểu cách quyết định nơi xác định trạng thái theo cách mà bạn có thể sử dụng lại và chia sẻ thành phần kết hợp.

Trong một hàm có khả năng kết hợp, bạn có thể xác định biến duy trì trạng thái để hiển thị trong giao diện người dùng. Ví dụ: bạn xác định biến amountInput là trạng thái trong thành phần kết hợp EditNumberField().

Khi ứng dụng trở nên phức tạp hơn và các thành phần kết hợp khác cần truy cập vào trạng thái trong thành phần kết hợp EditNumberField(), bạn cần cân nhắc chuyển lên trên hoặc trích xuất trạng thái ra khỏi hàm có khả năng kết hợp EditNumberField().

Hiểu thành phần kết hợp có trạng thái so với không có trạng thái

Bạn nên chuyển trạng thái lên trên khi cần:

  • Chia sẻ trạng thái với nhiều hàm có khả năng kết hợp.
  • Tạo một thành phần kết hợp không có trạng thái mà bạn có thể sử dụng lại trong ứng dụng.

Khi bạn trích xuất trạng thái từ một hàm có khả năng kết hợp thì kết quả là hàm có khả năng kết hợp đó được gọi là không có trạng thái. Điều này nghĩa là bạn có thể tạo ra các hàm có khả năng kết hợp phi trạng thái bằng cách trích xuất trạng thái từ các hàm đó.

Thành phần kết hợp phi trạng thái là thành phần kết hợp không có trạng thái, không duy trì, xác định hoặc sửa đổi trạng thái mới. Mặt khác, thành phần kết hợp có tính trạng thái là thành phần kết hợp có một phần trạng thái thay đổi được theo thời gian.

State hoisting (Chuyển trạng thái lên trên) là mẫu chuyển trạng thái cho phương thức gọi để làm cho một thành phần phi trạng thái.

Khi áp dụng cho các thành phần kết hợp, điều này thường có nghĩa là đưa hai tham số vào thành phần kết hợp:

  • Tham số value: T, là giá trị hiện tại để hiển thị.
  • Một onValueChange: (T) -> Unit – lệnh gọi lại lambda, được kích hoạt khi giá trị thay đổi để trạng thái có thể được cập nhật ở nơi khác, chẳng hạn như khi người dùng nhập một số văn bản vào hộp văn bản.

Chuyển trạng thái lên trên trong hàm EditNumberField():

  1. Cập nhật định nghĩa hàm EditNumberField() để chuyển trạng thái lên trên bằng cách thêm tham số valueonValueChange:
@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
//...

Tham số value thuộc loại String và tham số onValueChange thuộc loại (String) -> Unit, vì vậy đây là một hàm nhận giá trị String làm giá trị nhập và không có giá trị trả về. Tham số onValueChange được dùng khi lệnh gọi lại onValueChange truyền vào thành phần kết hợp TextField.

  1. Trong hàm EditNumberField(), hãy cập nhật hàm TextField() có khả năng kết hợp để dùng các tham số được truyền vào:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. Chuyển trạng thái lên trên, di chuyển trạng thái nhớ từ hàm EditNumberField() sang hàm TipTimeLayout():
@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }

   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       //...
   ) {
       //...
   }
}
  1. Bạn đã chuyển trạng thái lên trên vào TipTimeLayout(), giờ hãy truyền trạng thái vào EditNumberField(). Trong hàm TipTimeLayout(), hãy cập nhật lệnh gọi hàm EditNumberField() để dùng trạng thái được chuyển lên trên:
EditNumberField(
   value = amountInput,
   onValueChange = { amountInput = it },
   modifier = Modifier
       .padding(bottom = 32.dp)
       .fillMaxWidth()
)

Thao tác này sẽ làm cho EditNumberField không có trạng thái. Bạn đã chuyển trạng thái giao diện người dùng lên trên đối tượng cấp trên là TipTimeLayout(). TipTimeLayout() hiện có trạng thái (amountInput).

Định dạng vị trí

Định dạng vị trí dùng để hiện nội dung động trong các chuỗi. Ví dụ: giả sử bạn muốn hộp văn bản Tip amount (Số tiền boa) hiện giá trị xx.xx, có thể là bất kỳ số tiền nào được tính và định dạng trong hàm. Để thực hiện điều này trong tệp strings.xml, bạn cần xác định tài nguyên chuỗi bằng một đối số phần giữ chỗ, chẳng hạn như đoạn mã dưới đây:

// No need to copy.

// In the res/values/strings.xml file
<string name="tip_amount">Tip Amount: %s</string>

Trong mã Compose, bạn có thể có nhiều đối số phần giữ chỗ thuộc bất kỳ loại nào. Phần giữ chỗ string%s.

Hãy chú ý đến thành phần kết hợp văn bản trong TipTimeLayout(), bạn truyền tiền boa được định dạng làm đối số cho hàm stringResource().

// No need to copy
Text(
   text = stringResource(R.string.tip_amount, "$0.00"),
   style = MaterialTheme.typography.displaySmall
)
  1. Trong hàm TipTimeLayout(), hãy dùng thuộc tính tip để hiện số tiền boa. Cập nhật tham số text của thành phần kết hợp Text để dùng biến tip làm tham số.
Text(
     text = stringResource(R.string.tip_amount, tip),
     // ...

Các hàm TipTimeLayout()EditNumberField() hoàn chỉnh sẽ trông như đoạn mã dưới đây:

@Composable
fun TipTimeLayout() {
   var amountInput by remember { mutableStateOf("") }
   val amount = amountInput.toDoubleOrNull() ?: 0.0
   val tip = calculateTip(amount)

   Column(
       modifier = Modifier
            .statusBarsPadding()
            .padding(horizontal = 40.dp)
            .verticalScroll(rememberScrollState())
            .safeDrawingPadding(),
       horizontalAlignment = Alignment.CenterHorizontally,
       verticalArrangement = Arrangement.Center
   ) {
       Text(
           text = stringResource(R.string.calculate_tip),
           modifier = Modifier
               .padding(bottom = 16.dp, top = 40.dp)
               .align(alignment = Alignment.Start)
       )
       EditNumberField(
           value = amountInput,
           onValueChange = { amountInput = it },
           modifier = Modifier
               .padding(bottom = 32.dp)
               .fillMaxWidth()
       )
       Text(
           text = stringResource(R.string.tip_amount, tip),
           style = MaterialTheme.typography.displaySmall
       )
       Spacer(modifier = Modifier.height(150.dp))
   }
}

@Composable
fun EditNumberField(
   value: String,
   onValueChange: (String) -> Unit,
   modifier: Modifier = Modifier
) {
   TextField(
       value = value,
       onValueChange = onValueChange,
       singleLine = true,
       label = { Text(stringResource(R.string.bill_amount)) },
       keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
       modifier = modifier
   )
}

Tóm lại, bạn chuyển trạng thái amountInput lên trên từ EditNumberField() vào thành phần kết hợp TipTimeLayout(). Để hộp văn bản hoạt động như trước, bạn phải truyền 2 đối số vào hàm có khả năng kết hợp EditNumberField(): giá trị amountInput và lệnh gọi lại lambda cập nhật giá trị amountInput từ giá trị nhập vào của người dùng. Các thay đổi này cho phép bạn tính tiền boa từ thuộc tính amountInput trong TipTimeLayout() để hiện tiền boa cho người dùng.

  1. Chạy ứng dụng trên trình mô phỏng hoặc thiết bị rồi nhập giá trị vào hộp văn bản số tiền trên hoá đơn. Số tiền boa bằng 15% số tiền trên hoá đơn sẽ xuất hiện như bạn thấy trong ảnh dưới đây:

b6bd5374911410ac.png

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

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

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-tip-calculator.git
$ cd basic-android-kotlin-compose-training-tip-calculator
$ git checkout state

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ở tệp đó trong Android Studio.

Nếu bạn muốn thấy mã giải pháp, hãy xem mã đó trên GitHub.

12. Kết luận

Xin chúc mừng! Bạn đã hoàn thành lớp học lập trình này và học cách sử dụng trạng thái trong ứng dụng Compose!

Tóm tắt

  • Trạng thái trong ứng dụng là giá trị bất kỳ có thể thay đổi theo thời gian.
  • Thành phần kết hợp là nội dung mô tả về giao diện người dùng do Compose tạo ra khi thực thi các thành phần kết hợp. Các ứng dụng Compose gọi các hàm có khả năng kết hợp để biến đổi dữ liệu thành giao diện người dùng.
  • Thành phần kết hợp ban đầu là sản phẩm giao diện người dùng mà Compose tạo ra khi thực thi hàm có khả năng kết hợp lần đầu tiên.
  • Kết hợp lại là quá trình chạy cùng các thành phần kết hợp một lần nữa để cập nhật cây khi dữ liệu của các thành phần này thay đổi.
  • State hoisting (Chuyển trạng thái lên trên) là mẫu chuyển trạng thái cho phương thức gọi để làm cho một thành phần phi trạng thái.

Tìm hiểu thêm