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:
Đ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
Row
vàColumn
. - 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
- 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.
- 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.
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:
- Mở dự án bằng mã khởi đầu trong Android Studio.
- Chạy ứng dụng trên thiết bị Android hoặc trình mô phỏng.
- 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.
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ợpspacer
để 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 định15.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()
}
}
Lấy thông tin nhập 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:
Ứ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 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:
Thêm thành phần kết hợp TextField
vào ứng dụng:
- Trong tệp
MainActivity.kt
, hãy thêm một hàmEditNumberField()
có khả năng kết hợp. Hàm này sẽ lấy tham sốModifier
. - Trong phần nội dung của hàm
EditNumberField()
bên dướiTipTimeLayout()
, hãy thêmTextField
chấp nhận một tham số có tênvalue
được đặt thành chuỗi trống và một tham số có tênonValueChange
được đặt thành biểu thức lambda trống:
@Composable
fun EditNumberField(modifier: Modifier = Modifier) {
TextField(
value = "",
onValueChange = {},
modifier = modifier
)
}
- 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.
- Nhập hàm dưới đây:
import androidx.compose.material3.TextField
- 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àmEditNumberField()
, 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.
- 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ảnTip Amount
.
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ữ:
- Ở đầu hàm
EditNumberField()
, dùng từ khoával
để thêm biếnamountInput
, đặ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.
- Đặt tham số có tên
value
thành giá trịamountInput
:
TextField(
value = amountInput,
onValueChange = {},
)
- 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:
- 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ínhamountInput
.
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 State
và MutableState
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ụ:
- Trong hàm
EditNumberField()
, thay đổi từ khoával
trước biến trạng tháiamountInput
thành từ khoávar
:
var amountInput = "0"
Thao tác này có thể thay đổi amountInput
.
- Dùng loại
MutableState<String>
thay vì biếnString
được cố định giá trị trong mã để Compose biết cần theo dõi trạng tháiamountInput
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áiamountInput
:
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.
- Trong hàm có khả năng kết hợp
TextField
, sử dụng thuộc tínhamountInput.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.
- Trong biểu thức lambda của tham số có tên
onValueChange
, đặt thuộc tínhamountInput.value
thành biếnit
:
@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
.
- 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:
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 remember
và mutableStateOf
đượ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()
:
- Trong hàm
EditNumberField()
, khởi tạo biếnamountInput
có uỷ quyền thuộc tính Kotlinby
remember
, bằng cách bao quanh lệnh gọi đến hàmmutableStateOf
()
bằngremember
. - 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
.
- 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
)
}
- 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.
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ị:
- Trong hàm
EditNumberField()
bên cạnh tham số có tênonValueChange
, đặt điểm ngắt dòng. - 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.
- Trong ngăn Gỡ lỗi, nhấp vào Tiếp tục chương trình. Hộp văn bản đã được tạo.
- 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.
- Trong ngăn Gỡ lỗi, nhấp vào 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:
Đây là trạng thái của trường văn bản.
- Nhấp vào 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.
Sửa đổi hàm EditNumberField()
để thêm nhãn vào trường văn bản:
- Trong hàm có khả năng kết hợp
TextField()
của hàmEditNumberField()
, thêm tham số có tênlabel
được đặt thành biểu thức lambda trống:
TextField(
//...
label = { }
)
- Trong biểu thức lambda, hãy gọi hàm
Text()
chấp nhậnstringResource
(R.string.
bill_amount
)
:
label = { Text(stringResource(R.string.bill_amount)) },
- Trong hàm
TextField()
có khả năng kết hợp, hãy thêm tham số có tênsingleLine
đượ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.
- Thêm tập hợp tham số
keyboardOptions
vàoKeyboardOptions()
:
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.
- Đặ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ênkeyboardType
được đặt thànhKeyboardType.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
)
}
- 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:
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.
- 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ĩaamountInput
. Gọi hàmtoDoubleOrNull
trên biếnamountInput
để chuyển đổiString
thànhDouble
:
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ố.
- Ở cuối câu lệnh, thêm một toán tử Elvis
?:
trả về giá trị0.0
khiamountInput
có giá trị rỗng (null):
val amount = amountInput.toDoubleOrNull() ?: 0.0
- Sau biến
amount
, tạo một biếnval
khác có tên làtip
. Khởi chạy vớicalculateTip()
, 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:
- Trong hàm
TipTimeLayout()
ở cuối khốiColumn()
, 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ã:
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:
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()
:
- 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ốvalue
vàonValueChange
:
@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
.
- Trong hàm
EditNumberField()
, hãy cập nhật hàmTextField()
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
)
- Chuyển trạng thái lên trên, di chuyển trạng thái nhớ từ hàm
EditNumberField()
sang hàmTipTimeLayout()
:
@Composable
fun TipTimeLayout() {
var amountInput by remember { mutableStateOf("") }
val amount = amountInput.toDoubleOrNull() ?: 0.0
val tip = calculateTip(amount)
Column(
//...
) {
//...
}
}
- 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àoEditNumberField()
. Trong hàmTipTimeLayout()
, hãy cập nhật lệnh gọi hàmEditNumberField
()
để 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
là %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
)
- Trong hàm
TipTimeLayout()
, hãy dùng thuộc tínhtip
để hiện số tiền boa. Cập nhật tham sốtext
của thành phần kết hợpText
để dùng biếntip
làm tham số.
Text(
text = stringResource(R.string.tip_amount, tip),
// ...
Các hàm TipTimeLayout()
và 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.
- 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:
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.