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ề state và cách có thể sử dụng và thao tác state trong Jetpack Compose.

Về cơ bản, state 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, bao quát mọi thứ từ một cơ sở dữ liệu cho đến một biến trong ứng dụng của bạn. Bạn sẽ được tìm hiểu kỹ hơn về cơ sở dữ liệu ở bài học sau, nhưng hiện tại, tất cả những gì bạn cần biết là cơ sở dữ liệu là một tập hợp thông tin có cấu trúc được tổ chức ngăn nắp, chẳng hạn như các tệp trên máy tính của bạn.

Tất cả ứng dụng Android đều cho người dùng thấy state. Sau đây là một số ví dụ về state 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ý. State 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. State 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 state 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 composable TextField để nhập và chỉnh sửa văn bản.
  • Một composable Text để hiện văn bản.
  • Một composable 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:

e82cbb534872abcf.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ư composable bố cục RowColumn.
  • Quen thuộc cơ bản với đối tượng sửa đổi, chẳng hạn như hàm Modifier.padding().
  • Quen thuộc với composable Text.

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

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

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.

46bf4366edc1055f.png 18da3c120daa0759.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.

c0980ba3e9ebba02.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 khoá 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 tuỳ 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 thành phần dành cho nhãn và thành phần còn lại dùng để hiện số tiền boa.

e85b767a43c69a97.png

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

Mã khởi đầu có các composable 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 composable 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ó composable 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()
   }
}

ae11354e61d2a2b9.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 hình ảnh dưới đây:

58671affa01fb9e1.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 composable cho phép người dùng nhập văn bản vào một ứ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 hình ả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 composable TextField vào ứng dụng:

  1. Trong tệp MainActivity.kt, hãy thêm một hàm composable EditNumberField(). 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 composable TipTimeLayout(), trên dòng sau hàm composable văn bản đầu tiê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à composable văn bản Tip Amount.

2c208378cd4b8d41.png

4. Dùng state trong Compose

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

Thêm biến vào state 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à state 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 state như bạn thấy trong hình ảnh dưới đây:

e8e24821adfd9d8c.png

  1. Khi chạy ứng dụng trong trình mô phỏng, hãy nhập một giá trị khác. State được cố định giá trị trong mã vẫn không thay đổi vì composable 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 state của hộp văn bản. State đượ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 state của ứng dụng khi người dùng cập nhật số tiền trên hoá đơn.

5. Composition

Các composable trong ứng dụng của bạn mô tả một giao diện người dùng gồm có một cột chứa một vài đoạn văn bản, một khoảng trống 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 composable 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 recomposition để cập nhật Composition của ứng dụng.

Composition là bản mô tả về giao diện người dùng do Compose tạo ra khi thực thi các composable. Các ứng dụng Compose gọi các hàm composable để biến đổi dữ liệu thành giao diện người dùng. Nếu có thay đổi về state, thì Compose sẽ thực thi lại các hàm composable bị ảnh hưởng với state 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à recomposition. Compose lên lịch recomposition cho bạn.

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

Composition chỉ có thể được quá trình composition ban đầu tạo ra và cập nhật bằng quá trình recomposition. Cách duy nhất để sửa đổi một Composition là recomposition. Để làm điều này, Compose cần biết state nào cần theo dõi để có thể lên lịch recomposition 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 recomposition.

Bạn sử dụng loại StateMutableState trong Compose để làm cho Compose có thể quan sát hoặc theo dõi state 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ì state, 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 recomposition để cập nhật giao diện người dùng.

Thêm state chi phí dịch vụ:

  1. Trong hàm EditNumberField(), thay đổi từ khoá val trước biến state 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 state amountInput và sau đó truyền vào chuỗi "0", vốn là giá trị mặc định ban đầu của biến state 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 composable TextField, sử dụng thuộc tính amountInput.value:
TextField(
   value = amountInput.value,
   onValueChange = {},
   modifier = modifier
)

Compose theo dõi từng composable đọc thuộc tính value của state và kích hoạt quá trình recomposition 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 state 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 state amountInput, do đó ngay khi giá trị của state này thay đổi, quá trình recomposition sẽ được lên lịch và hàm composable EditNumberField() sẽ được thực thi lại. Trong hàm composable đó, 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 state sẽ khiến quá trình recomposition đượ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 recomposition để biến này không được đặt lại thành giá trị 0 mỗi khi hàm EditNumberField() recompose. 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 state

Các phương thức composable có thể được gọi nhiều lần do quá trình recomposition. Composable đặt lại state trong quá trình recomposition nếu không được lưu.

Các hàm composable có thể lưu trữ đối tượng trong quá trình recomposition bằng remember. Một giá trị do hàm remember tính toán được lưu trữ trong thành phần Composition trong quá trình composition ban đầu và giá trị đã lưu trữ được trả về trong quá trình recomposition. Thông thường, hàm remembermutableStateOf được dùng cùng nhau trong hàm composable để state 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 Uỷ quyền thuộc tính Kotlin. Các hàm getter và setter mặc định cho thuộc tính amountInput được uỷ 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.

59ac301a208b47c4.png

7. State và composable 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 composable EditNumberField() để xem quá trình composition bản đầu và recomposition 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.

154e060231439307.png

  1. Trong ngăn Gỡ lỗi, nhấp vào 2a29a3bad712bec.png 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 Recomposition với dữ liệu mới khi giá trị quan sát được đã thay đổi.

1d5e08d32052d02e.png

  1. Trong ngăn Gỡ lỗi, nhấp vào 2a29a3bad712bec.png Tiếp tục chương trình. Có thể thấy văn bản được nhập trong trình mô phỏng hoặc trên thiết bị hiển thị bên cạnh dòng có điểm ngắt trong hình ảnh sau:

1f5db6ab5ca5b477.png

Đây là state của trường văn bản.

  1. Nhấp vào 2a29a3bad712bec.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, xem Kết cấu.

a2afd6c7fc547b06.png

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

  1. Trong hàm composable 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 composable TextField(), 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:

55936268bf007ee9.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 composable 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 composable EditNumberField(), 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 composable 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à state của trường văn bản được xác định trong hàm composable EditNumberField(), 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ã:

50bf0b9d18ede6be.png

Cấu trúc này sẽ không cho phép bạn hiện số tiền boa trong composable Text mới vì composable 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 composable EditNumberField() stateless:

ab4ec72388149f7c.png

Mô hình này được gọi là state hoisting (chuyển state lên trên). Trong phần tiếp theo, bạn hoist, tức là nâng state từ một composable để làm cho composable đó là stateless.

10. State hoisting (chuyển state 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 state theo cách mà bạn có thể sử dụng lại và chia sẻ composable.

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

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

Tìm hiểu sự khác biệt giữa composable stateful và stateless

Bạn nên chuyển state lên trên khi cần:

  • Chia sẻ state với nhiều hàm composable.
  • Tạo một composable stateless mà bạn có thể sử dụng lại trong ứng dụng.

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

Một composable stateless là composable không có state, không duy trì, xác định hoặc sửa đổi state mới. Mặt khác, một composable stateful là composable có một phần state có thể thay đổi theo thời gian.

State hoisting (Chuyển state lên trên) là một mô hình di chuyển state lên phương thức gọi của nó để làm cho một thành phần trở thành stateless.

Khi áp dụng cho các composable, điều này thường có nghĩa là đưa hai tham số vào composable:

  • 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 để state 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 state lên trên trong hàm EditNumberField():

  1. Cập nhật định nghĩa hàm EditNumberField() để chuyển state 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 composable TextField.

  1. Trong hàm EditNumberField(), hãy cập nhật hàm composable TextField()để dùng các tham số được truyền vào:
TextField(
   value = value,
   onValueChange = onValueChange,
   // Rest of the code
)
  1. Chuyển state lên trên, di chuyển state 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 state lên trên vào TipTimeLayout(), giờ hãy truyền state vào EditNumberField(). Trong hàm TipTimeLayout(), hãy cập nhật lệnh gọi hàm EditNumberField() để dùng state đượ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 thành stateless. Bạn đã chuyển state giao diện người dùng lên trên đối tượng cấp trên là TipTimeLayout(). Giờ đây, TipTimeLayout() là đối tượng nắm giữ state (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 composable 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 composable 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 state amountInput lên trên từ EditNumberField() vào composable 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 composable 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 hình ảnh dưới đây:

de593783dc813e24.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 muốn xem đoạn mã giải pháp, bạn có thể xem 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 state trong ứng dụng Compose!

Tóm tắt

  • State trong ứng dụng là giá trị bất kỳ có thể thay đổi theo thời gian.
  • Composition là bản mô tả về giao diện người dùng do Compose tạo ra khi thực thi các composable. Các ứng dụng Compose gọi các hàm composable để biến đổi dữ liệu thành giao diện người dùng.
  • Thành phần composition 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 composable lần đầu tiên.
  • Recomposition là quá trình chạy cùng các composable 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 state lên trên) là một mô hình di chuyển state lên phương thức gọi của nó để làm cho một thành phần trở thành stateless.

Tìm hiểu thêm