1. Trước khi bắt đầu
Lớp học lập trình này giải thích các khái niệm chính liên quan đến việc sử dụng Trạng thái trong Jetpack Compose. Qua đó, giúp bạn nắm được cách trạng thái của ứng dụng xác định nội dung xuất hiện trên giao diện người dùng, cách Compose làm việc với API để cập nhật giao diện người dùng khi trạng thái thay đổi, cách tối ưu hoá cấu trúc của hàm có khả năng kết hợp cũng như cách sử dụng ViewModel trong thế giới Compose.
Điều kiện tiên quyết
- Có kiến thức về cú pháp Kotlin.
- Hiểu biết cơ bản về Compose (bạn có thể bắt đầu với hướng dẫn về Jetpack Compose).
- Hiểu biết cơ bản về
ViewModel
của Thành phần kiến trúc.
Kiến thức bạn sẽ học được
- Tìm hiểu cách hoạt động của trạng thái và sự kiện trong giao diện người dùng Jetpack Compose.
- Cách Compose sử dụng trạng thái để xác định các phần tử hiển thị trên màn hình.
- Chuyển trạng thái lên trên.
- Cách các hàm có khả năng kết hợp có trạng thái và không có trạng thái hoạt động.
- Cách Compose tự động theo dõi trạng thái bằng API
State<T>
. - Cách bộ nhớ và trạng thái nội bộ hoạt động trong một hàm có khả năng kết hợp: sử dụng API
remember
vàrememberSaveable
. - Cách xử lý danh sách và trạng thái: sử dụng API
mutableStateListOf
vàtoMutableStateList
. - Cách sử dụng
ViewModel
bằng Compose.
Bạn cần có
Đề xuất/không bắt buộc
- Đọc bài viết Tư duy trong Compose.
- Vui lòng xem lớp học lập trình cơ bản về Jetpack Compose trước khi tham gia lớp học lập trình này Chúng ta sẽ làm tóm tắt toàn bộ Trạng thái trong lớp học lập trình này.
Sản phẩm bạn sẽ tạo ra
Bạn sẽ triển khai một ứng dụng Wellness đơn giản:
Ứng dụng sẽ có hai chức năng chính:
- Một đồng hồ nước để theo dõi lượng nước bạn uống.
- Danh sách các nhiệm vụ để chăm sóc sức khoẻ mỗi ngày.
Để được hỗ trợ thêm khi tham gia lớp học lập trình này, hãy xem các nội dung tập lập trình dưới đây:
2. Bắt đầu thiết lập
Bắt đầu một dự án Compose mới
- Để bắt đầu một dự án Compose mới, vui lòng mở Android Studio.
- Nếu bạn đang ở cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), vui lòng nhấp vào nút Start a new Android Studio (Bắt đầu dự án Android Studio mới). Nếu bạn đã mở một dự án Android Studio, hãy chọn File > New > New Project (Tệp > Mới > Dự án mới) trong thanh trình đơn.
- Đối với dự án mới, hãy chọn Empty Activity (Hoạt động trống) trong số các mẫu có sẵn.
- Nhấp vào Next (Tiếp theo) và định cấu hình dự án, gọi dự án là "BasicStateCodelab".
Hãy nhớ chọn minimumSdkVersion tối thiểu là API cấp 21, đây là API tối thiểu mà Compose hỗ trợ.
Khi bạn chọn mẫu Empty Compose Activity (Hoạt động trống trong Compose), Android Studio sẽ thiết lập các tài nguyên sau cho bạn trong dự án:
- Một lớp
MainActivity
được định cấu hình bằng hàm có khả năng kết hợp hiển thị một số văn bản trên màn hình. - Tệp
AndroidManifest.xml
xác định quyền, thành phần và tài nguyên tuỳ chỉnh của ứng dụng. - Các tệp
build.gradle.kts
vàapp/build.gradle.kts
chứa các lựa chọn và phần phụ thuộc cần thiết cho Compose.
Giải pháp cho lớp học lập trình
Bạn có thể lấy mã giải pháp cho BasicStateCodelab
từ GitHub:
$ git clone https://github.com/android/codelab-android-compose
Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp Zip.
Bạn sẽ tìm thấy đoạn mã giải pháp trong dự án BasicStateCodelab
. Bạn nên làm theo hướng dẫn từng bước trong lớp học lập trình theo tốc độ riêng và xem phần giải pháp nếu cần trợ giúp. Xuyên suốt lớp học lập trình, bạn sẽ thấy các đoạn mã bạn cần thêm vào dự án.
3. Trạng thái trong Compose
"Trạng thái" của ứng dụng là giá trị bất kỳ có thể thay đổi theo thời gian. Đây là định nghĩa rất rộng và bao gồm mọi thứ từ cơ sở dữ liệu Room cho đến một biến trên một lớp (class).
Tất cả ứng dụng Android đều cho người dùng thấy trạng thái. Dưới đây là một số ví dụ về trạng thái trong ứng dụng Android:
- Tin nhắn nhận được gần đây nhất trong ứng dụng trò chuyện.
- Ảnh hồ sơ của người dùng.
- Vị trí cuộn trong danh sách các mục.
Hãy bắt đầu viết ứng dụng Sức khoẻ của bạn.
Để đơn giản hoá, trong lớp học lập trình này:
- Bạn có thể thêm tất cả tệp Kotlin trong gói
com.codelabs.basicstatecodelab
gốc của mô-đunapp
. Tuy nhiên, đối với ứng dụng chính thức, tệp phải được cấu trúc hợp lý trong các gói con. - Bạn sẽ mã hoá cứng tất cả các chuỗi cùng dòng trong đoạn mã. Trong một ứng dụng thực tế, bạn nên thêm các tài nguyên này dưới dạng tài nguyên chuỗi trong tệp
strings.xml
, đồng thời tham chiếu bằng APIstringResource
của Compose.
Chức năng đầu tiên bạn cần xây dựng là bộ đếm nước để đếm số lượng ly nước bạn tiêu thụ trong ngày.
Tạo một hàm có khả năng kết hợp có tên là WaterCounter
chứa thành phần kết hợp Text
cho thấy số lượng ly nước. Số lượng ly nước sẽ được lưu trữ trong một giá trị có tên là count
. Hiện tại, bạn có thể mã hoá cứng các ly nước này.
Tạo một tệp mới WaterCounter.kt
bằng hàm có khả năng kết hợp WaterCounter
như bên dưới:
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
val count = 0
Text(
text = "You've had $count glasses.",
modifier = modifier.padding(16.dp)
)
}
Hãy tạo một hàm có khả năng kết hợp đại diện cho toàn bộ màn hình, gồm hai phần là bộ đếm nước và danh sách nhiệm vụ chăm sóc sức khoẻ. Giờ thì chúng ta chỉ cần thêm bộ đếm.
- Tạo một tệp
WellnessScreen.kt
đại diện cho màn hình chính, sau đó gọi hàmWaterCounter
:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
WaterCounter(modifier)
}
- Mở
MainActivity.kt
. Xoá các thành phần kết hợpGreeting
vàDefaultPreview
. Gọi thành phần kết hợpWellnessScreen
mới tạo bên trong khốisetContent
của Hoạt động, như bên dưới:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
BasicStateCodelabTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
WellnessScreen()
}
}
}
}
}
- Nếu chạy ứng dụng ngay thì bạn sẽ thấy màn hình bộ đếm nước cơ bản có số lượng ly nước được mã hoá cứng.
Trạng thái của hàm có khả năng kết hợp WaterCounter
là biến count
. Tuy nhiên, việc có trạng thái tĩnh không hữu ích lắm vì không thể sửa đổi được. Để khắc phục sự cố này, bạn sẽ thêm tham số Button
để tăng số lượng và theo dõi lượng nước bạn uống trong ngày.
Bất kỳ hành động nào dẫn đến việc sửa đổi trạng thái đều được gọi là "sự kiện", chúng ta sẽ tìm hiểu thêm về các sự kiện này trong phần tiếp theo.
4. Sự kiện trong Compose
Chúng ta đã nói về trạng thái dưới dạng mọi giá trị thay đổi theo thời gian (ví dụ: tin nhắn cuối cùng nhận được trong ứng dụng nhắn tin). Nhưng điều gì giúp cập nhật trạng thái? Trong các ứng dụng Android, trạng thái được cập nhật để phản hồi các sự kiện.
Sự kiện là các thông tin đầu vào được tạo từ bên ngoài hoặc bên trong một ứng dụng, chẳng hạn như:
- Người dùng tương tác với giao diện người dùng bằng cách nhấn một nút chẳng hạn.
- Các yếu tố khác, chẳng hạn như cảm biến gửi giá trị mới hoặc phản hồi mạng.
Mặc dù trạng thái của ứng dụng cung cấp mô tả về nội dung sẽ hiển thị trong giao diện người dùng, nhưng sự kiện là cơ chế thay đổi của trạng thái, dẫn đến các thay đổi đối với giao diện người dùng.
Sự kiện sẽ thông báo cho một phần của chương trình là có điều gì đó đã xảy ra. Tất cả ứng dụng Android đều có một vòng lặp cập nhật giao diện người dùng cốt lõi như bên dưới:
- Sự kiện – Một sự kiện do người dùng hoặc một phần khác của chương trình tạo ra.
- Trạng thái cập nhật – Trình xử lý sự kiện thay đổi trạng thái mà giao diện người dùng sử dụng.
- Trạng thái hiển thị – Giao diện người dùng được cập nhật để hiển thị trạng thái mới.
Quản lý trạng thái trong Compose nghĩa là hiểu được cách các trạng thái và sự kiện tương tác với nhau.
Bây giờ, hãy thêm nút này để người dùng có thể sửa đổi trạng thái bằng cách thêm nhiều ly nước.
Chuyển đến hàm có khả năng kết hợp WaterCounter
để thêm Button
bên dưới nhãn Text
. Column
sẽ giúp bạn căn chỉnh Text
theo chiều dọc với thành phần kết hợp Button
. Bạn có thể di chuyển khoảng đệm bên ngoài vào thành phần kết hợp Column
, sau đó thêm một số khoảng đệm bổ sung vào đầu Button
để nội dung đó được tách khỏi Văn bản.
Hàm có khả năng kết hợp Button
sẽ nhận được một hàm lambda onClick
– đây là sự kiện xảy ra khi người dùng nhấp vào nút này. Bạn sẽ thấy các ví dụ khác về hàm lambda ở những phần sau.
Hãy thay đổi count
thành var
thay vì val
để nó có thể thay đổi.
import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count = 0
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Khi chạy ứng dụng và nhấp vào nút, bạn sẽ nhận thấy rằng không có gì xảy ra. Việc đặt một giá trị khác cho biến count
sẽ không làm cho Compose phát hiện biến này là một thay đổi về trạng thái, nên sẽ không có gì xảy ra. Điều này là do bạn chưa yêu cầu Compose vẽ lại màn hình (tức là "kết hợp lại" hàm có khả năng kết hợp) khi trạng thái thay đổi. Bạn sẽ khắc phục tình trạng này trong bước tiếp theo.
5. Bộ nhớ trong hàm có khả năng kết hợp
Ứng dụng Compose biến đổi dữ liệu thành giao diện người dùng bằng cách gọi các hàm có khả năng kết hợp. Chúng tôi gọi Cấu trúc 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. 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, tạo ra một giao diện người dùng cập nhật. Đây gọi là quá trình kết hợp lại. Compose cũng xem xét những dữ liệu cần thiết cho một thành phần kết hợp riêng lẻ, để nó chỉ cần kết hợp lại các thành phần có dữ liệu đã thay đổi và bỏ qua những thành phần không bị ảnh hưởng.
Để 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 bản cập nhật.
Compose có một hệ thống theo dõi trạng thái đặc biệt để lên lịch kết hợp lại cho bất cứ thành phần kết hợp nào đọc một trạng thái cụ thể. Điều này sẽ chi tiết hoá Compose và chỉ kết hợp lại các hàm có khả năng kết hợp cần thay đổi, chứ không phải toàn bộ giao diện người dùng. Việc này được thực hiện bằng cách không chỉ theo dõi "ghi" (nghĩa là thay đổi trạng thái), mà còn "đọc" trạng thái đó.
Sử dụng các loại State
và MutableState
trong Compose để khiến trạng thái có thể quan sát được bằng Compose.
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. 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 đó giúp value
có thể quan sát được.
Cập nhật thành phần kết hợp WaterCounter
để count
sử dụng API mutableStateOf
với 0
làm giá trị ban đầu. Khi mutableStateOf
trả về một loại MutableState
, bạn có thể cập nhật value
để cập nhật trạng thái, và Compose sẽ kích hoạt quá trình kết hợp lại các hàm đó khi value
được đọc.
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
// Changes to count are now tracked by Compose
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Như đã đề cập trước đó, mọi thay đổi đối với count
sẽ lên lịch kết hợp lại mọi hàm có khả năng kết hợp và tự động đọc value
của count
. Trong trường hợp này, WaterCounter
sẽ được kết hợp lại mỗi khi người dùng nhấp vào nút này.
Nếu chạy ứng dụng ngay thì bạn sẽ nhận thấy chưa có điều gì xảy ra!
Việc lên lịch kết hợp lại đang hoạt động tốt. Tuy nhiên, khi quá trình kết hợp lại xảy ra, biến count
được khởi tạo sẽ trở về 0, vậy nên chúng ta cần giải pháp nào đó để lưu giữ giá trị này trên các quá trình kết hợp lại.
Để làm được điều này, chúng ta có thể dùng hàm cùng dòng có thể kết hợp remember
. Cấu trúc lưu giữ giá trị do remember
tính toán trong quá trình kết hợp ban đầu. Giá trị đã lưu trữ được giữ lại qua các lần kết hợp lại.
Thường thì remember
và mutableStateOf
được dùng cùng nhau trong các hàm có khả năng kết hợp.
Có một vài cách tương đương để viết mã này như minh hoạ trong tài liệu về Trạng thái Compose.
Sửa đổi WaterCounter
, bao quanh lệnh gọi đến mutableStateOf
bằng hàm có khả năng kết hợp cùng dòng remember
:
import androidx.compose.runtime.remember
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = remember { mutableStateOf(0) }
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Ngoài ra, chúng ta có thể đơn giản hoá việc sử dụng count
bằng cách dùng các thuộc tính được uỷ quyền của Kotlin.
Bạn có thể sử dụng từ khoá by để xác định count
dưới dạng một var. Việc thêm lệnh nhập getter và setter của phần tử uỷ quyền cho phép chúng ta đọc và thay đổi count
một cách gián tiếp mà không cần tham chiếu rõ ràng đến thuộc tính value
của MutableState
mọi lần.
WaterCounter
hiện sẽ có dạng như sau:
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
Text("You've had $count glasses.")
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Bạn nên chọn cú pháp tạo ra mã dễ đọc nhất trong thành phần kết hợp mà bạn đang viết.
Giờ chúng ta sẽ xem lại những gì đã làm được nhé:
- Đã xác định một biến theo thời gian được gọi là
count
. - Đã tạo một màn hình hiển thị văn bản để cho người dùng biết số mà chúng ta đã nhớ.
- Đã thêm một nút làm tăng số lượng mà chúng ta đã nhớ bất cứ khi nào nó được nhấp vào.
Sự sắp xếp này tạo thành một vòng hồi tiếp luồng dữ liệu với người dùng:
- Giao diện người dùng sẽ hiển thị trạng thái cho người dùng (số lượng hiện tại được hiển thị dưới dạng văn bản).
- Người dùng tạo các sự kiện được kết hợp với trạng thái hiện có để tạo ra trạng thái mới (việc nhấp vào nút sẽ thêm một sự kiện vào số lượng hiện tại)
Bộ đếm của bạn đã sẵn sàng và hoạt động!
6. Giao diện người dùng hướng trạng thái
Compose là một khung giao diện người dùng khai báo. Thay vì xoá các thành phần giao diện người dùng hoặc thay đổi chế độ hiển thị khi trạng thái thay đổi, chúng ta sẽ mô tả giao diện người dùng trông như thế nào trong các điều kiện cụ thể của trạng thái. Do một quá trình kết hợp lại đang được gọi và giao diện người dùng được cập nhật, các thành phần kết hợp có thể được nhập hoặc rời khỏi Cấu trúc.
Phương pháp này giúp bạn dễ dàng cập nhật chế độ xem theo cách thủ công như áp dụng với hệ thống Chế độ xem. Ngoài ra, chế độ xem này cũng ít gặp lỗi hơn vì bạn không thể quên cập nhật chế độ xem dựa trên trạng thái mới do chế độ này tự động diễn ra.
Nếu một hàm có khả năng kết hợp được gọi trong cấu trúc ban đầu, hoặc trong các thành phần kết hợp lại, thì hàm đó có trong Cấu trúc. Một hàm có khả năng kết hợp không được gọi (ví dụ: vì hàm được gọi bên trong câu lệnh if và điều kiện này không được đáp ứng) sẽ không có trong Cấu trúc.
Bạn có thể tìm hiểu thêm về vòng đời của các thành phần kết hợp trong tài liệu.
Đầu ra của Cấu trúc là một cấu trúc dạng cây mô tả giao diện người dùng.
Bạn có thể kiểm tra bố cục ứng dụng do Compose tạo bằng cách sử dụng công cụ Layout Inspector của Android Studio, đây cũng là việc tiếp theo bạn sẽ thực hiện.
Để chứng minh điều này, hãy sửa đổi mã để hiển thị giao diện người dùng dựa trên trạng thái. Mở WaterCounter
và hiển thị Text
nếu count
lớn hơn 0:
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
// This text is present if the button has been clicked
// at least once; absent otherwise
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
Chạy ứng dụng và mở công cụ Layout Inspector của Android Studio bằng cách chuyển đến Tools (Công cụ) > Layout Inspector (Trình kiểm tra Bố cục).
Bạn sẽ thấy một màn hình chia đôi: cây thành phần ở bên trái và bản xem trước ứng dụng ở bên phải.
Nhấn vào phần tử gốc BasicStateCodelabTheme
ở bên trái màn hình để di chuyển đến cây. Mở rộng toàn bộ cây thành phần bằng cách nhấp vào nút Expand all (Mở rộng tất cả).
Nhấp vào một phần tử trong màn hình ở bên phải để chuyển đến phần tử tương ứng của cây.
Nếu bạn nhấn nút Add one (Thêm một) trên ứng dụng:
- Số lượng sẽ tăng lên 1 và trạng thái thay đổi.
- Quá trình kết hợp lại được gọi.
- Màn hình được kết hợp lại với các thành phần mới.
Khi kiểm tra cây thành phần bằng công cụ Layout Inspector của Android Studio, bạn cũng sẽ thấy thành phần kết hợp Text
:
Trạng thái sẽ điều khiển các phần tử có trong giao diện người dùng tại một thời điểm nhất định.
Các phần khác nhau của giao diện người dùng có thể phụ thuộc vào cùng một trạng thái. Sửa đổi Button
để nó được bật cho đến khi count
là 10, sau đó tắt (và bạn đã đạt được mục tiêu của mình trong ngày). Hãy dùng tham số enabled
của Button
để thực hiện việc này.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
...
}
Chạy ứng dụng ngay. Các thay đổi đối với count
trạng thái sẽ xác định liệu có cho thấy Text
hay không và liệu Button
được bật hay tắt.
7. Ghi nhớ trong Cấu trúc
remember
lưu trữ các đối tượng trong Cấu trúc và quên đối tượng nếu vị trí nguồn nơi remember
được gọi không được gọi lại trong quá trình kết hợp lại.
Để trực quan hoá hành vi này, bạn sẽ triển khai chức năng sau đây trong ứng dụng: khi người dùng đã uống ít nhất một ly nước, ứng dụng cho thấy một nhiệm vụ chăm sóc sức khoẻ mà người dùng cần thực hiện cũng như cho phép họ đóng nhiệm vụ đó. Vì thành phần kết hợp phải có kích thước nhỏ và có thể sử dụng lại, hãy tạo một thành phần kết hợp mới tên là WellnessTaskItem
để cho thấy nhiệm vụ chăm sóc sức khoẻ đó dựa trên chuỗi nhận được dưới dạng tham số, cùng với nút biểu tượng Đóng.
Tạo tệp mới WellnessTaskItem.kt
và thêm vào mã sau. Bạn sẽ sử dụng các hàm có khả năng kết hợp này sau trong lớp học lập trình này.
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding
@Composable
fun WellnessTaskItem(
taskName: String,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.weight(1f).padding(start = 16.dp),
text = taskName
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
Hàm WellnessTaskItem
nhận được nội dung mô tả tác vụ và hàm lambda onClose
(cũng giống như thành phần kết hợp Button
tích hợp sẵn sẽ nhận được onClick
).
WellnessTaskItem
sẽ có dạng như sau:
Để cải thiện ứng dụng bằng việc bổ sung thêm tính năng, hãy cập nhật WaterCounter
để hiển thị WellnessTaskItem
khi count
> 0.
Khi count
lớn hơn 0, hãy xác định một biến showTask
giúp xác định xem có hiển thị WellnessTaskItem
hay không và khởi chạy biến đó thành giá trị true.
Thêm câu lệnh if mới để hiển thị WellnessTaskItem
nếu showTask
là giá trị true. Sử dụng các API bạn đã tìm hiểu ở những phần trước để đảm bảo giá trị showTask
vẫn tồn tại sau khi kết hợp lại.
@Composable
fun WaterCounter() {
Column(modifier = Modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
}
}
Dùng hàm lambda onClose
của WellnessTaskItem
để khi nhấn nút X, biến showTask
sẽ thay đổi thành false
và tác vụ không còn xuất hiện nữa.
...
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
...
Tiếp theo, hãy thêm Button
mới có nội dung "Clear water count" (Xoá lượng nước) rồi đặt bên cạnh nút "Add one" (Thêm một) Button
. Row
có thể giúp căn chỉnh hai nút. Bạn cũng có thể thêm một số khoảng đệm vào Row
. Khi nhấn nút "Clear water count" (Xoá lượng nước), biến count
được đặt lại về 0.
Hàm WaterCounter
đã hoàn tất sẽ có dạng như sau.
import androidx.compose.foundation.layout.Row
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
Khi bạn chạy ứng dụng, màn hình sẽ hiển thị trạng thái ban đầu:
Ở bên phải, chúng tôi có một phiên bản đơn giản của cây thành phần, giúp bạn phân tích những gì đang xảy ra khi trạng thái thay đổi. count
và showTask
là các giá trị được ghi nhớ.
Giờ bạn có thể làm theo các bước sau trong ứng dụng:
- Nhấn vào nút Thêm một. Việc này làm tăng
count
(dẫn đến quá trình kết hợp lại) và khiến cảWellnessTaskItem
lẫn bộ đếmText
bắt đầu hiển thị.
- Nhấn vào X của thành phần
WellnessTaskItem
(dẫn tới một quá trình kết hợp lại khác).showTask
hiện đang là giá trị false, nghĩa làWellnessTaskItem
không còn được hiển thị nữa.
- Nhấn vào nút Add one (Thêm một) (một quá trình kết hợp lại khác).
showTask
ghi nhớ bạn đã đóngWellnessTaskItem
trong các lần tái cấu trúc tiếp theo nếu bạn tiếp tục thêm số ly nước.
- Nhấn nút Xoá lượng nước để đặt lại
count
về 0 và tạo thành phần kết hợp lại.Text
hiển thịcount
và mọi mã liên quan đếnWellnessTaskItem
sẽ không được gọi và rời khỏi Cấu trúc.
showTask
bị bỏ qua vì vị trí mã (nơi hàm ghi nhớshowTask
được gọi) đã không được gọi. Bạn đang quay lại bước đầu tiên.
- Nhấn nút Add one (Thêm một) để tạo
count
lớn hơn 0 (kết hợp lại).
- Cấu trúc
WellnessTaskItem
sẽ xuất hiện lại vì giá trịshowTask
trước đó đã bị quên khi rời khỏi Cấu trúc nêu trên.
Điều gì sẽ xảy ra nếu chúng ta yêu cầu showTask
vẫn tồn tại sau khi count
quay về 0, lâu hơn mức mà remember
cho phép (nghĩa là ngay cả khi vị trí mã – nơi remember
được gọi – không được gọi trong quá trình kết hợp lại)? Chúng ta sẽ tìm hiểu cách khắc phục những trường hợp này và nhiều ví dụ khác trong các phần tiếp theo.
Bây giờ, bạn đã hiểu cách đặt lại giao diện người dùng và trạng thái khi thoát khỏi thành phần Compose, hãy xoá mã của bạn rồi quay lại WaterCounter
bạn đã có ở đầu phần này:
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
8. Khôi phục trạng thái trong Compose
Chạy ứng dụng, thêm một vài ly nước vào bộ đếm rồi xoay thiết bị. Đảm bảo bạn đã bật chế độ Tự động xoay của thiết bị.
Do Activity (Hoạt động) được tạo lại sau khi thay đổi cấu hình (trong trường hợp này là hướng) nên trạng thái lưu đã bị quên: bộ đếm sẽ biến mất khi quay về 0.
Điều tương tự cũng xảy ra nếu bạn thay đổi ngôn ngữ, chuyển đổi giữa chế độ tối và sáng hoặc thực hiện bất cứ thay đổi khác về cấu hình khiến Android tạo lại Activity (Hoạt động) đang chạy.
Mặc dù remember
giúp bạn giữ lại trạng thái qua các lần kết hợp lại, nhưng trạng thái này không được giữ lại khi bạn thay đổi cấu hình. Để thực hiện việc này, bạn phải sử dụng rememberSaveable
thay vì remember
.
rememberSaveable
tự động lưu mọi giá trị có thể lưu trong Bundle
. Đối với các giá trị khác, bạn có thể truyền vào một đối tượng lưu tuỳ chỉnh. Để biết thêm thông tin về cách Khôi phục trạng thái trong Compose, vui lòng xem tài liệu.
Trong WaterCounter
, hãy thay thế remember
bằng rememberSaveable
:
import androidx.compose.runtime.saveable.rememberSaveable
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
...
var count by rememberSaveable { mutableStateOf(0) }
...
}
Chạy ứng dụng ngay và thử thực hiện một số thay đổi về cấu hình. Bạn sẽ thấy bộ đếm được lưu đúng cách.
Tạo lại hoạt động chỉ là một trong những trường hợp sử dụng của rememberSaveable
. Chúng ta sẽ tìm hiểu một trường hợp sử dụng khác sau khi xử lý các danh sách.
Cân nhắc xem nên sử dụng remember
hay rememberSaveable
tuỳ thuộc vào trạng thái và nhu cầu trải nghiệm người dùng của ứng dụng.
9. Chuyển trạng thái lên trên (state hoisting)
Một thành phần kết hợp sử dụng remember
để lưu trữ một đối tượng sẽ tạo trạng thái nội bộ, giúp thành phần kết hợp có trạng thái. Điều này có thể hữu ích trong trường hợp phương thức gọi không cần kiểm soát trạng thái và có thể sử dụng mà không phải tự quản lý trạng thái. Tuy nhiên, các thành phần kết hợp với trạng thái nội bộ có xu hướng ít có khả năng tái sử dụng và khó thử nghiệm hơn.
Các thành phần kết hợp không có trạng thái nào được gọi là thành phần kết hợp không có trạng thái (stateless composable). Một cách dễ dàng để tạo thành phần kết hợp không có trạng thái là sử dụng tính năng chuyển trạng thái lên trên.
Tính năng chuyển trạng thái lên trên (state hoisting) trong Compose là một dạng chuyển đổi trạng thái cho phương thức gọi của một thành phần kết hợp khiến nó trở thành không trạng thái. Mô hình chung cho việc di chuyển trạng thái lên trên trong Jetpack Compose là thay thế biến trạng thái bằng hai tham số:
- value: T – giá trị hiện tại để hiển thị
- onValueChange: (T) -> Unit – một sự kiện yêu cầu giá trị thay đổi, trong đó T là giá trị mới
nơi giá trị này thể hiện cho bất kỳ trạng thái nào có thể sửa đổi.
Trạng thái được di chuyển lên trên theo cách này có một số thuộc tính quan trọng:
- Một nguồn đáng tin cậy (single source of truth): Bằng cách di chuyển trạng thái thay vì sao chép, chúng tôi đảm bảo rằng chỉ có một nguồn thông tin duy nhất. Điều này giúp tránh các lỗi.
- Có thể chia sẻ (shareable): Bạn có thể chia sẻ trạng thái được di chuyển lên trên với nhiều thành phần kết hợp.
- Có thể chắn (interceptable): Phương thức gọi đến các thành phần kết hợp không trạng thái có thể quyết định bỏ qua hoặc sửa đổi các sự kiện trước khi thay đổi trạng thái.
- Decoupled (tách riêng): Trạng thái cho hàm có khả năng kết hợp không có trạng thái, có thể được lưu trữ ở bất cứ đâu. Ví dụ như trong ViewModel.
Hãy cố gắng triển khai trạng thái này cho WaterCounter
để có thể hưởng lợi từ tất cả các thuộc tính trên.
Có trạng thái so với Không có trạng thái
Khi tất cả trạng thái có thể được trích xuất từ một hàm có khả năng kết hợp, hàm đó được gọi là hàm không có trạng thái.
Tái cấu trúc thành phần kết hợp WaterCounter
bằng cách chia thành hai phần: Bộ đếm có trạng thái và không có trạng thái.
Vai trò của StatelessCounter
là hiển thị count
và gọi một hàm khi bạn tăng count
. Để thực hiện việc này, hãy làm theo mẫu được mô tả ở trên và chuyển trạng thái, count
(dưới dạng tham số đến hàm có khả năng kết hợp) và hàm lambda (onIncrement
), được gọi khi trạng thái cần được tăng lên. StatelessCounter
sẽ có dạng như sau:
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
StatefulCounter
sở hữu trạng thái này. Nghĩa là nó giữ trạng thái count
và sửa đổi khi gọi hàm StatelessCounter
.
@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
var count by rememberSaveable { mutableStateOf(0) }
StatelessCounter(count, { count++ }, modifier)
}
Tốt lắm! Bạn đã nâng count
từ StatelessCounter
lên StatefulCounter
.
Bạn có thể cắm vào ứng dụng của mình và cập nhật WellnessScreen
bằng StatefulCounter
:
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
StatefulCounter(modifier)
}
Như đã đề cập, việc di chuyển trạng thái lên trên có một số lợi ích. Chúng ta sẽ khám phá các biến thể của mã này để giải thích một số biến thể đó, bạn không cần phải sao chép các đoạn mã sau trong ứng dụng.
- Thành phần kết hợp không có trạng thái hiện có thể được sử dụng lại. Hãy lấy ví dụ bên dưới.
Để đếm các ly nước và nước ép, bạn nhớ waterCount
và juiceCount
, nhưng hãy sử dụng cùng một hàm có khả năng kết hợp StatelessCounter
để hiển thị hai trạng thái độc lập khác nhau.
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
Nếu juiceCount
được sửa đổi, thì StatefulCounter
sẽ được kết hợp lại. Trong quá trình kết hợp lại, Compose sẽ xác định những hàm có vai trò đọc juiceCount
và kích hoạt quá trình kết hợp lại chỉ với những hàm đó.
Khi người dùng nhấn để tăngjuiceCount
.StatefulCounter
kết hợp lại và do đó, StatelessCounter
sẽ có nội dung juiceCount
. Nhưng StatelessCounter
đọc waterCount
thì không được kết hợp lại.
- Hàm có khả năng kết hợp có trạng thái có thể cung cấp cùng một trạng thái cho nhiều hàm có khả năng kết hợp.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
Trong trường hợp này, nếu số lượng được cập nhật bởi StatelessCounter
hoặc AnotherStatelessMethod
, mọi hàm sẽ được kết hợp lại như dự kiến.
Vì trạng thái được chuyển lên trên có thể chia sẻ được, bạn phải nhớ chỉ chuyển trạng thái mà các thành phần kết hợp cần để tránh các thành phần kết hợp lại không cần thiết cũng như tăng khả năng tái sử dụng.
Để đọc thêm về trạng thái và việc chuyển trạng thái lên trên, vui lòng xem tài liệu về Trạng thái Compose.
10. Xử lý các danh sách
Tiếp theo, hãy thêm tính năng thứ hai cho ứng dụng của bạn là danh sách các nhiệm vụ về sức khoẻ. Bạn có thể thực hiện hai hành động với các mục trong danh sách:
- Đánh dấu các mục trong danh sách để cho biết nhiệm vụ đã hoàn thành.
- Xoá nhiệm vụ khỏi danh sách mà bạn không muốn hoàn thành.
Thiết lập
- Trước tiên, hãy sửa đổi mục danh sách. Bạn có thể sử dụng lại
WellnessTaskItem
ở phần Ghi nhớ trong Cấu trúc và cập nhật thành phần này để chứaCheckbox
. Hãy đảm bảo bạn nâng trạng tháichecked
và lệnh gọi lạionCheckedChange
để làm cho hàm không có trạng thái.
Thành phần kết hợp WellnessTaskItem
cho phần này sẽ có dạng như sau:
import androidx.compose.material3.Checkbox
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- Cũng trong tệp đó, hãy thêm một hàm có khả năng kết hợp
WellnessTaskItem
có trạng thái xác định một biến trạng tháicheckedState
, và chuyển biến đó vào phương thức không có trạng thái cùng tên. Đừng bận tâm vềonClose
hiện tại, bạn có thể truyền một hàm lambda trống.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
var checkedState by remember { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = {}, // we will implement this later!
modifier = modifier,
)
}
- Tạo tệp
WellnessTask.kt
để mô hình hoá một tác vụ chứa mã nhận dạng và nhãn. Hãy xác định lớp đó dưới dạng một lớp dữ liệu.
data class WellnessTask(val id: Int, val label: String)
- Đối với danh sách tác vụ, hãy tạo một tệp mới có tên là
WellnessTasksList.kt
rồi thêm phương thức tạo ra một số dữ liệu giả:
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
Lưu ý trong một ứng dụng thực tế, bạn sẽ nhận dữ liệu từ lớp dữ liệu.
- Trong
WellnessTasksList.kt
, hãy thêm một hàm có khả năng kết hợp tạo danh sách. Xác địnhLazyColumn
và các mục trong phương thức danh sách mà bạn đã tạo. Vui lòng xem tài liệu về Danh sách nếu bạn cần trợ giúp.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember
@Composable
fun WellnessTasksList(
modifier: Modifier = Modifier,
list: List<WellnessTask> = remember { getWellnessTasks() }
) {
LazyColumn(
modifier = modifier
) {
items(list) { task ->
WellnessTaskItem(taskName = task.label)
}
}
}
- Thêm danh sách này vào
WellnessScreen
. Sử dụngColumn
để giúp căn chỉnh danh sách theo chiều dọc với bộ đếm bạn đã có.
import androidx.compose.foundation.layout.Column
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList()
}
}
- Chạy ứng dụng và dùng thử! Giờ thì bạn đã có thể kiểm tra các tác vụ nhưng không thể xoá chúng. Bạn sẽ triển khai tính năng đó ở phần sau.
Khôi phục trạng thái của mục trong LazyList
Vui lòng xem xét kỹ hơn một số nội dung trong thành phần kết hợp WellnessTaskItem
.
checkedState
thuộc về từng thành phần kết hợp WellnessTaskItem
một cách độc lập, giống như một biến riêng. Khi checkedState
thay đổi, chỉ có phiên bản WellnessTaskItem
đó được kết hợp lại chứ không phải tất cả phiên bản WellnessTaskItem
trong LazyColumn
.
Hãy thử bằng cách làm theo các bước sau:
- Đánh dấu phần tử bất kỳ ở đầu danh sách này (ví dụ như các phần tử 1 và 2).
- Di chuyển xuống cuối danh sách để ra khỏi màn hình.
- Cuộn lại lên trên cùng các mục mà bạn đã chọn trước đó.
- Lưu ý bạn đã bỏ đánh dấu các hộp này.
Có một sự cố, như bạn đã thấy ở phần trước, khi một mục rời khỏi Cấu trúc, trạng thái được ghi nhớ sẽ bị bỏ qua. Các mục trên LazyColumn
sẽ hoàn toàn rời khỏi Cấu trúc khi bạn di chuyển qua các mục đó và chúng không còn xuất hiện nữa.
Bạn sẽ khắc phục vấn đề này bằng cách nào? Một lần nữa, hãy dùng rememberSaveable
. Trạng thái mà bạn chọn sẽ vẫn tồn tại trong quá trình tạo lại hoạt động hoặc quy trình thông qua việc sử dụng cơ chế trạng thái của thực thể đã lưu. Nhờ cách kết hợp của rememberSaveable
với LazyList
, các mục của bạn vẫn có khả năng tồn tại khi rời khỏi Cấu trúc.
Bạn chỉ cần thay thế remember
bằng rememberSaveable
trong WellnessTaskItem
trạng thái, và thế là xong:
import androidx.compose.runtime.saveable.rememberSaveable
var checkedState by rememberSaveable { mutableStateOf(false) }
Các mẫu phổ biến trong Compose
Lưu ý việc triển khai LazyColumn
:
@Composable
fun LazyColumn(
...
state: LazyListState = rememberLazyListState(),
...
Hàm có khả năng kết hợp rememberLazyListState
tạo trạng thái ban đầu cho danh sách bằng rememberSaveable
. Khi Hoạt động được tạo lại, trạng thái cuộn được duy trì mà bạn không cần phải lập trình.
Nhiều ứng dụng cần phản ứng và tuân theo vị trí cuộn, thay đổi bố cục mục cũng như các sự kiện khác liên quan đến trạng thái của danh sách. Các thành phần Lazy, chẳng hạn như LazyColumn
hoặc LazyRow
, hỗ trợ trường hợp sử dụng này thông qua việc nâng cấp LazyListState
. Bạn có thể tìm hiểu thêm về mẫu này trong tài liệu về trạng thái trong danh sách.
Việc lấy tham số trạng thái có giá trị mặc định do hàm rememberX
công khai cung cấp là một mẫu hình phổ biến trong các hàm có khả năng kết hợp được tích hợp sẵn. Bạn có thể tìm thấy một ví dụ khác trong BottomSheetScaffold
, theo đó trạng thái được di chuyển lên trên bằng rememberBottomSheetScaffoldState
.
11. Danh sách MutableList có thể quan sát được
Tiếp theo, để thêm hành vi xoá một tác vụ khỏi danh sách của chúng tôi, trước tiên, bạn cần đặt danh sách đó thành một danh sách có thể thay đổi.
Bạn không sử dụng được đối tượng có thể thay đổi cho mục này, chẳng hạn như ArrayList<T>
hoặc mutableListOf,
. Loại đối tượng này sẽ không thông báo cho Compose về các mục trong danh sách đã thay đổi và lên lịch kết hợp lại giao diện người dùng. Bạn cần một API khác.
Bạn cần tạo một thực thể MutableList
có thể quan sát được bằng Compose. Cấu trúc này cho phép Compose theo dõi các thay đổi để tái cấu trúc giao diện người dùng khi các mục được thêm vào hoặc bị xoá khỏi danh sách.
Bắt đầu bằng cách xác định MutableList
có thể quan sát được. Hàm mở rộng toMutableStateList()
là cách tạo MutableList
có thể quan sát được từ Collection
có thể hoặc không thể thay đổi ban đầu, chẳng hạn như List
.
Ngoài ra, bạn cũng có thể sử dụng phương thức trạng thái ban đầu mutableStateListOf
để tạo MutableList
có thể quan sát được, sau đó thêm các phần tử cho trạng thái ban đầu.
- Mở tệp
WellnessScreen.kt
. Hãy di chuyển phương thứcgetWellnessTasks
sang tệp này để có thể sử dụng. Tạo danh sách bằng cách gọigetWellnessTasks()
trước rồi sử dụng hàm mở rộngtoMutableStateList
mà bạn đã tìm hiểu trước đó.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- Hãy sửa đổi hàm có khả năng kết hợp
WellnessTasksList
bằng cách xoá giá trị mặc định của danh sách, vì danh sách được chuyển lên cấp màn hình. Thêm một tham số hàm lambda mớionCloseTask
(nhậnWellnessTask
để xoá ). TruyềnonCloseTask
vàoWellnessTaskItem
.
Bạn cần thực hiện một thay đổi nữa. Phương thức items
nhận được tham số key
. Theo mặc định, trạng thái của mỗi mục được khoá dựa vào vị trí của mục trong danh sách.
Trong danh sách có thể thay đổi, việc này sẽ gây ra sự cố khi tập dữ liệu thay đổi, vì các mục thay đổi vị trí sẽ mất bất kỳ trạng thái nào đã được ghi nhớ.
Bạn có thể dễ dàng khắc phục vấn đề này bằng cách sử dụng id
của từng WellnessTaskItem
làm khoá cho mỗi mục.
Để tìm hiểu thêm về nội dung khoá mục trong một danh sách, vui lòng xem tài liệu.
WellnessTasksList
sẽ có dạng như sau:
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(modifier = modifier) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
}
}
}
- Sửa đổi
WellnessTaskItem
: thêm hàm lambdaonClose
dưới dạng tham số vàoWellnessTaskItem
trạng thái và gọi hàm đó.
@Composable
fun WellnessTaskItem(
taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
var checkedState by rememberSaveable { mutableStateOf(false) }
WellnessTaskItem(
taskName = taskName,
checked = checkedState,
onCheckedChange = { newValue -> checkedState = newValue },
onClose = onClose,
modifier = modifier,
)
}
Tốt lắm! Chức năng đã hoàn tất và xoá được một mục khỏi danh sách hoạt động.
Nếu bạn nhấp vào dấu X trong mỗi hàng, thì các sự kiện sẽ chuyển đến danh sách sở hữu trạng thái, xoá mục khỏi danh sách và khiến Compose kết hợp lại màn hình.
Nếu cố gắng sử dụng rememberSaveable()
để lưu trữ danh sách trong WellnessScreen
thì bạn sẽ nhận được một ngoại lệ cho thời gian chạy:
Lỗi này cho biết bạn phải cung cấp trình lưu tuỳ chỉnh. Tuy nhiên, bạn không nên sử dụng rememberSaveable
để lưu trữ một lượng lớn dữ liệu hoặc cấu trúc dữ liệu phức tạp đòi hỏi quá trình chuyển đổi tuần tự hoặc huỷ chuyển đổi tuần tự dài.
Các quy tắc tương tự áp dụng khi làm việc với onSaveInstanceState
của Hoạt động; bạn có thể xem thêm thông tin trong tài liệu về Lưu trạng thái giao diện người dùng. Nếu làm như vậy, bạn cần có một cơ chế lưu trữ thay thế. Bạn có thể tìm hiểu thêm về nhiều phương án giúp duy trì trạng thái giao diện người dùng trong tài liệu.
Tiếp theo, chúng ta sẽ xem vai trò của ViewModel là phần tử sở hữu trạng thái của ứng dụng.
12. Trạng thái trong ViewModel
Màn hình hoặc trạng thái giao diện người dùng cho biết nội dung sẽ hiển thị trên màn hình (ví dụ như danh sách tác vụ). Trạng thái này thường được kết nối với các lớp khác trong hệ phân cấp vì nó chứa dữ liệu ứng dụng..
Mặc dù trạng thái giao diện người dùng mô tả nội dung xuất hiện trên màn hình, nhưng logic của ứng dụng lại mô tả cách ứng dụng hoạt động và sẽ phản ứng với các thay đổi về trạng thái. Có hai loại logic: logic hành vi trên giao diện người dùng hoặc logic giao diện người dùng, và logic kinh doanh.
- Logic giao diện người dùng liên quan đến các thay đổi về trạng thái cách hiển thị trên màn hình (ví dụ như logic điều hướng hoặc hiển thị thanh thông báo nhanh).
- Logic kinh doanh là những việc nên làm trước những thay đổi về trạng thái (ví dụ: thanh toán hoặc lưu trữ lựa chọn ưu tiên của người dùng). Logic này thường được đặt trong các lớp nghiệp vụ hoặc dữ liệu, không bao giờ được đặt trong lớp giao diện người dùng.
ViewModel cho biết trạng thái của giao diện người dùng cũng như quyền tiếp cận logic nghiệp vụ trong các lớp khác của ứng dụng. Ngoài ra, ViewModel giữ lại các thay đổi về cấu hình nên chúng có thời gian tồn tại lâu hơn so với Cấu trúc. Chúng có thể tuân theo vòng đời của máy chủ lưu trữ nội dung Compose (tức là các hoạt động, mảnh hoặc đích đến của Biểu đồ điều hướng) nếu bạn đang sử dụng tính năng Điều hướng Compose.
Để tìm hiểu thêm về cấu trúc và lớp giao diện người dùng, vui lòng xem tài liệu về lớp giao diện người dùng.
Di chuyển danh sách và xoá phương thức
Mặc dù các bước trước cho bạn biết cách quản lý trạng thái trực tiếp trong các Hàm có khả năng kết hợp, nhưng bạn nên tách biệt logic giao diện người dùng và logic nghiệp vụ với trạng thái giao diện người dùng rồi di chuyển sang một ViewModel.
Hãy di chuyển trạng thái giao diện người dùng, danh sách sang ViewModel, đồng thời bắt đầu trích xuất logic nghiệp vụ vào đó.
- Tạo một tệp
WellnessViewModel.kt
để thêm lớp ViewModel.
Di chuyển getWellnessTasks()
"nguồn dữ liệu" của bạn sang WellnessViewModel
.
Xác định biến _tasks
nội bộ, sử dụng toMutableStateList
như bạn đã làm trước đây và cho thấy tasks
dưới dạng danh sách, để không sửa đổi được thuộc tính này từ bên ngoài ViewModel.
Triển khai một hàm remove
đơn giản có thể uỷ quyền cho hàm xoá tích hợp trong danh sách.
import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel
class WellnessViewModel : ViewModel() {
private val _tasks = getWellnessTasks().toMutableStateList()
val tasks: List<WellnessTask>
get() = _tasks
fun remove(item: WellnessTask) {
_tasks.remove(item)
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
- Chúng ta có thể truy cập ViewModel này từ bất kỳ thành phần kết hợp nào bằng cách gọi hàm
viewModel()
.
Để dùng hàm này, hãy mở tệp app/build.gradle.kts
, thêm thư viện sau đây rồi đồng bộ hoá các phần phụ thuộc mới trong Android Studio:
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")
Sử dụng phiên bản 2.6.2
đối với Android Studio Giraffe. Nếu không, hãy kiểm tra phiên bản mới nhất của thư viện tại đây.
- Mở
WellnessScreen
. Tạo bản sao ViewModel củawellnessViewModel
bằng cách gọiviewModel()
dưới dạng tham số của thành phần kết hợp Màn hình. Bản sao này có thể được thay thế khi thử nghiệm thành phần kết hợp và được nâng lên nếu cần. Cung cấp choWellnessTasksList
danh sách tác vụ cũng như xoá hàm khỏi lambdaonCloseTask
.
import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCloseTask = { task -> wellnessViewModel.remove(task) })
}
}
viewModel()
trả về một chế độ ViewModel
hiện có hoặc tạo một chế độ mới trong phạm vi nhất định. Thực thể ViewModel được giữ lại miễn là phạm vi vẫn hoạt động. Ví dụ như nếu sử dụng thành phần kết hợp trong một hoạt động, viewModel()
sẽ trả về cùng một phiên bản cho đến khi hoạt động đó kết thúc hoặc quá trình kết thúc.
Chỉ vậy thôi! Bạn đã tích hợp ViewModel với một phần trạng thái và logic kinh doanh với màn hình của mình. Vì trạng thái được giữ bên ngoài Cấu trúc và do ViewModel lưu trữ, nên các thay đổi đối với danh sách sẽ vẫn tồn tại sau khi thay đổi cấu hình.
ViewModel sẽ không tự động duy trì trạng thái của ứng dụng trong bất cứ trường hợp nào (ví dụ: quá trình bị buộc tắt do hệ thống gây ra). Để biết thông tin chi tiết về cách lưu giữ trạng thái giao diện người dùng của ứng dụng, vui lòng xem tài liệu.
Di chuyển trạng thái đã đánh dấu
Tái cấu trúc gần đây nhất nghĩa là di chuyển trạng thái và logic đã đánh dấu sang ViewModel. Bằng cách này, mã sẽ đơn giản và dễ kiểm thử hơn nhờ tất cả trạng thái do ViewModel quản lý.
- Trước tiên, hãy sửa đổi lớp mô hình
WellnessTask
để lớp này có thể lưu trữ trạng thái đã đánh dấu và đặt giá trị mặc định là false.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
- Trong ViewModel, hãy triển khai một phương thức
changeTaskChecked
nhận tác vụ cần sửa đổi với một giá trị mới cho trạng thái đã đánh dấu.
class WellnessViewModel : ViewModel() {
...
fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
_tasks.find { it.id == item.id }?.let { task ->
task.checked = checked
}
}
- Trong
WellnessScreen
, cung cấp hành vi choonCheckedTask
của danh sách bằng cách gọi phương thứcchangeTaskChecked
trong ViewModel. Các hàm giờ đây sẽ có dạng như sau:
@Composable
fun WellnessScreen(
modifier: Modifier = Modifier,
wellnessViewModel: WellnessViewModel = viewModel()
) {
Column(modifier = modifier) {
StatefulCounter()
WellnessTasksList(
list = wellnessViewModel.tasks,
onCheckedTask = { task, checked ->
wellnessViewModel.changeTaskChecked(task, checked)
},
onCloseTask = { task ->
wellnessViewModel.remove(task)
}
)
}
}
- Mở
WellnessTasksList
rồi thêm tham số hàm lambdaonCheckedTask
để bạn có thể truyền tham số đó xuốngWellnessTaskItem.
@Composable
fun WellnessTasksList(
list: List<WellnessTask>,
onCheckedTask: (WellnessTask, Boolean) -> Unit,
onCloseTask: (WellnessTask) -> Unit,
modifier: Modifier = Modifier
) {
LazyColumn(
modifier = modifier
) {
items(
items = list,
key = { task -> task.id }
) { task ->
WellnessTaskItem(
taskName = task.label,
checked = task.checked,
onCheckedChange = { checked -> onCheckedTask(task, checked) },
onClose = { onCloseTask(task) }
)
}
}
}
- Dọn dẹp tệp
WellnessTaskItem.kt
. Chúng ta không cần một phương thức trạng thái nữa, vì trạng thái CheckBox sẽ được chuyển lên cấp Danh sách. Tệp chỉ có hàm có khả năng kết hợp này:
@Composable
fun WellnessTaskItem(
taskName: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
onClose: () -> Unit,
modifier: Modifier = Modifier
) {
Row(
modifier = modifier, verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
text = taskName
)
Checkbox(
checked = checked,
onCheckedChange = onCheckedChange
)
IconButton(onClick = onClose) {
Icon(Icons.Filled.Close, contentDescription = "Close")
}
}
}
- Chạy ứng dụng rồi thử kiểm tra tác vụ bất kỳ. Xin lưu ý rằng chức năng kiểm tra tác vụ vẫn chưa hoạt động hiệu quả.
Lý do là vì đối tượng mà Compose đang theo dõi cho MutableList
là các thay đổi liên quan đến việc thêm và xoá phần tử. Vì vậy, chức năng xoá mới hoạt động. Nhưng chức năng này không nhận biết về các thay đổi trong giá trị của mục hàng (là checkedState
trong trường hợp này), trừ phi bạn cũng yêu cầu theo dõi các giá trị đó.
Có hai cách để khắc phục vấn đề này:
- Thay đổi lớp dữ liệu
WellnessTask
đểcheckedState
trở thànhMutableState<Boolean>
thay vìBoolean
, điều này khiến Compose theo dõi sự thay đổi về mục. - Sao chép mục bạn sắp thay đổi, xoá mục đó khỏi danh sách rồi thêm lại mục đó vào danh sách. Thao tác này giúp Compose theo dõi thay đổi đó.
Cả hai phương pháp đều có những ưu và nhược điểm. Ví dụ như tuỳ thuộc vào cách bạn đang triển khai danh sách, việc xoá và đọc phần tử có thể gây tốn kém.
Giả sử bạn muốn tránh các hoạt động danh sách có thể tốn kém và làm cho checkedState
có thể quan sát được vì cách này hiệu quả hơn và tương thích với Compose hơn.
WellnessTask
mới của bạn có thể có dạng như sau:
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))
Như đã thấy trước đó, bạn có thể sử dụng các thuộc tính được uỷ quyền, giúp việc sử dụng biến checked
đơn giản hơn cho trường hợp này.
Thay đổi WellnessTask
thành một lớp thay vì lớp dữ liệu. Thiết lập để WellnessTask
nhận một biến initialChecked
có giá trị mặc định false
trong hàm khởi tạo, sau đó, chúng ta có thể khởi tạo biến checked
bằng phương thức ban đầu mutableStateOf
rồi thiết lập initialChecked
làm giá trị mặc định.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
class WellnessTask(
val id: Int,
val label: String,
initialChecked: Boolean = false
) {
var checked by mutableStateOf(initialChecked)
}
Vậy là xong! Giải pháp này khá hiệu quả, mà tất cả thay đổi vẫn tồn tại sau khi kết hợp lại và thay đổi cấu hình!
Kiểm thử
Giờ đây, logic nghiệp vụ sẽ được tái cấu trúc thành ViewModel thay vì được kết hợp bên trong các hàm có thể kết hợp, việc kiểm thử đơn vị sẽ đơn giản hơn nhiều.
Bạn có thể sử dụng kiểm thử đo lường để xác minh hành vi chính xác của mã Compose cũng như trạng thái giao diện người dùng đang hoạt động đúng cách. Hãy cân nhắc tham gia lớp học lập trình Kiểm thử trong Compose để tìm hiểu cách kiểm thử giao diện người dùng trong Compose.
13. Xin chúc mừng
Tốt lắm! Bạn đã hoàn tất thành công lớp học lập trình này và tìm hiểu tất cả các API cơ bản để xử lý trạng thái trong ứng dụng Jetpack Compose!
Bạn đã tìm hiểu cách hoạt động của trạng thái và sự kiện để trích xuất các thành phần kết hợp không có trạng thái trong Compose, cũng như cách Compose sử dụng thông tin cập nhật trạng thái để thúc đẩy thay đổi trong giao diện người dùng.
Tiếp theo là gì?
Hãy tham khảo các lớp học lập trình khác trên Lộ trình học tập về Compose.
Ứng dụng mẫu
- JetNews trình bày các phương pháp hay nhất được giải thích trong lớp học lập trình này.
Tài liệu khác
- Tư duy trong Compose
- Trạng thái và Jetpack Compose
- Luồng dữ liệu một chiều trong Jetpack Compose
- Khôi phục trạng thái trong Compose
- Tổng quan về ViewModel
- Compose và các thư viện khác