Trong Compose, giao diện người dùng không thể thay đổi được, không có cách nào để cập nhật giao diện người dùng sau khi vẽ. Tuy nhiên, bạn có thể kiểm soát trạng thái của giao diện người dùng. Mỗi khi trạng thái của giao diện người dùng thay đổi, Compose sẽ tạo lại các phần của cây giao diện người dùng đã thay đổi. Các thành phần có thể kết hợp có thể chấp nhận trạng thái và hiển thị sự kiện — ví dụ: TextField
chấp nhận một giá trị và hiển thị một lệnh gọi lại onValueChange
yêu cầu trình xử lý gọi lại thay đổi giá trị.
var name by remember { mutableStateOf("") } OutlinedTextField( value = name, onValueChange = { name = it }, label = { Text("Name") } )
Vì các thành phần có thể kết hợp chấp nhận trạng thái và hiển thị sự kiện nên mẫu luồng dữ liệu một chiều rất phù hợp với Jetpack Compose. Bài hướng dẫn này tập trung vào cách triển khai mẫu luồng dữ liệu một chiều trong Compose, cách triển khai các sự kiện và trình sở hữu trạng thái, cũng như cách làm việc với ViewModels trong Compose.
Luồng dữ liệu một chiều
Luồng dữ liệu một chiều (UDF) là một mẫu thiết kế trong đó trạng thái chạy xuống và các sự kiện chạy lên. Bằng cách làm theo luồng dữ liệu một chiều, bạn có thể phân tách các thành phần có thể kết hợp hiển thị trạng thái trong giao diện người dùng khỏi các phần của ứng dụng lưu trữ và thay đổi trạng thái.
Vòng lặp cập nhật giao diện người dùng cho một ứng dụng sử dụng luồng dữ liệu một chiều như sau:
- Sự kiện: Một phần của giao diện người dùng tạo sự kiện và chuyển sự kiện lên trên, chẳng hạn như lượt nhấp vào nút được chuyển đến ViewModel để xử lý; hoặc một sự kiện được chuyển từ các lớp khác trong ứng dụng, chẳng hạn như cho biết phiên người dùng đã hết hạn.
- Trạng thái cập nhật: Trình xử lý sự kiện có thể thay đổi trạng thái.
- Trạng thái hiển thị: Trình sở hữu trạng thái chuyển xuống trạng thái và giao diện người dùng sẽ hiển thị trạng thái này.
![](https://developer.android.com/static/develop/ui/compose/images/state-unidirectional-flow.png?authuser=19&hl=vi)
Việc làm theo mẫu này khi sử dụng Jetpack Compose mang lại nhiều lợi ích:
- Khả năng thử nghiệm: Việc tách riêng trạng thái khỏi giao diện người dùng hiển thị trạng thái giúp bạn dễ dàng thử nghiệm riêng biệt cả hai.
- Sự đóng gói trạng thái: Vì trạng thái chỉ có thể được cập nhật ở một nơi duy nhất và chỉ có một nguồn thông tin đáng tin cậy cho trạng thái của một thành phần có thể kết hợp, nên ít có khả năng bạn sẽ gặp lỗi với trạng thái không nhất quán.
- Tính nhất quán của giao diện người dùng: Tất cả nội dung cập nhật trạng thái sẽ được phản ánh ngay lập tức trong giao diện người dùng bằng cách sử dụng
trình sở hữu trạng thái có thể quan sát, chẳng hạn như
StateFlow
hoặcLiveData
.
Luồng dữ liệu một chiều trong Jetpack Compose
Các thành phần có thể kết hợp hoạt động dựa trên trạng thái và sự kiện. Ví dụ: TextField
chỉ được cập nhật khi thông số value
được cập nhật và cho thấy một lệnh gọi lại onValueChange
— một sự kiện yêu cầu thay đổi giá trị thành một giá trị mới. Compose
xác định đối tượng State
là trình sở hữu giá trị và thay đổi đối với giá trị trạng thái
sẽ kích hoạt việc kết hợp lại. Bạn có thể giữ trạng thái trong
remember { mutableStateOf(value) }
hoặc
rememberSaveable { mutableStateOf(value)
tuỳ thuộc vào thời gian bạn cần
ghi nhớ giá trị.
Loại giá trị của thành phần kết hợp TextField
là String
, vì vậy, giá trị này có thể đến từ bất kỳ nơi nào, từ giá trị được mã hoá cứng, từ ViewModel hoặc được chuyển từ thành phần kết hợp mẹ. Bạn không cần phải giữ giá trị đó trong đối tượng State
, nhưng bạn cần
cập nhật giá trị khi onValueChange
được gọi.
Xác định các thông số có thể kết hợp
Khi xác định các thông số trạng thái của một thành phần có thể kết hợp, bạn nên lưu ý những câu hỏi sau:
- Thành phần có thể kết hợp có khả năng sử dụng lại hoặc linh hoạt ở mức nào?
- Các thông số trạng thái ảnh hưởng như thế nào đến hiệu suất của thành phần có thể kết hợp này?
Để ưu tiên phân tách và sử dụng lại, mỗi thành phần có thể kết hợp nên chứa ít thông tin nhất có thể. Ví dụ: khi tạo một thành phần có thể kết hợp để giữ tiêu đề của một tin bài, bạn chỉ nên chuyển thông tin cần hiển thị thay vì toàn bộ tin bài:
@Composable fun Header(title: String, subtitle: String) { // Recomposes when title or subtitle have changed. } @Composable fun Header(news: News) { // Recomposes when a new instance of News is passed in. }
Đôi khi, việc sử dụng từng thông số riêng lẻ cũng sẽ cải thiện hiệu quả hoạt động — ví dụ: nếu
News
chứa nhiều thông tin hơn so với title
và subtitle
, bất cứ khi nào
có một thực thể mới của News
được chuyển vào Header(news)
, thành phần có thể kết hợp sẽ
kết hợp lại, ngay cả khi title
và subtitle
chưa thay đổi.
Hãy cân nhắc kỹ số lượng thông số bạn chuyển vào. Việc có một hàm có quá nhiều thông số làm giảm khả năng sử dụng của hàm, vì vậy trong trường hợp này, việc nhóm các thông số này trong một lớp sẽ được ưu tiên.
Sự kiện trong Compose
Mọi đầu vào cho ứng dụng phải được thể hiện dưới dạng một sự kiện: lượt nhấn, thay đổi văn bản và thậm chí là bộ hẹn giờ hoặc bản cập nhật khác. Khi những sự kiện này thay đổi trạng thái trên giao diện người dùng,
ViewModel
phải là nút xử lý chúng và cập nhật trạng thái giao diện người dùng.
Lớp giao diện người dùng không được thay đổi trạng thái bên ngoài trình xử lý sự kiện vì việc này có thể gây ra sự không thống nhất và lỗi trong ứng dụng.
Ưu tiên chuyển các giá trị không thể thay đổi cho lambda của trình xử lý sự kiện và trạng thái. Phương pháp này mang lại các lợi ích sau:
- Bạn có thể cải thiện khả năng tái sử dụng.
- Bạn đảm bảo rằng giao diện người dùng không trực tiếp thay đổi giá trị của trạng thái.
- Bạn tránh các vấn đề đồng thời vì đảm bảo rằng trạng thái không thay đổi trong một chuỗi khác.
- Thông thường, bạn sẽ giảm bớt được độ phức tạp của mã.
Ví dụ: một thành phần có thể kết hợp chấp nhận String
và lambda vì các thông số có thể được gọi từ nhiều bối cảnh và có thể sử dụng lại nhiều. Giả sử rằng thanh ứng dụng
trên cùng trong ứng dụng luôn hiển thị văn bản và có nút quay lại. Bạn có thể xác định một
thành phần có thể kết hợp MyAppTopAppBar
tổng quát hơn sẽ nhận được văn bản và nút quay lại xử lý dưới dạng thông số:
@Composable fun MyAppTopAppBar(topAppBarText: String, onBackPressed: () -> Unit) { TopAppBar( title = { Text( text = topAppBarText, textAlign = TextAlign.Center, modifier = Modifier .fillMaxSize() .wrapContentSize(Alignment.Center) ) }, navigationIcon = { IconButton(onClick = onBackPressed) { Icon( Icons.Filled.ArrowBack, contentDescription = localizedString ) } }, // ... ) }
ViewModels, trạng thái và sự kiện: ví dụ
Bằng cách sử dụng ViewModel
và mutableStateOf
, bạn cũng có thể giới thiệu luồng dữ liệu một chiều trong ứng dụng nếu đáp ứng một trong các điều kiện sau:
- Trạng thái giao diện người dùng của bạn sẽ hiển thị qua các trình trạng thái có thể quan sát, như
StateFlow
hoặcLiveData
. ViewModel
xử lý các sự kiện đến từ giao diện người dùng hoặc các lớp khác trong ứng dụng và cập nhật trình sở hữu trạng thái dựa trên các sự kiện đó.
Ví dụ: khi triển khai màn hình đăng nhập, việc nhấn vào nút Đăng nhập sẽ khiến ứng dụng hiển thị vòng quay tiến trình và lệnh gọi mạng. Nếu đăng nhập thành công, thì ứng dụng sẽ chuyển đến một màn hình khác; trong trường hợp xảy ra lỗi, ứng dụng sẽ hiển thị một Thanh thông báo nhanh. Dưới đây là cách bạn sẽ mô hình hoá trạng thái màn hình và sự kiện:
Màn hình có 4 trạng thái:
- Đã đăng xuất: khi người dùng chưa đăng nhập.
- Đang tiến hành: khi ứng dụng đang cố gắng đăng nhập bằng cách thực hiện lệnh gọi mạng.
- Lỗi: khi xảy ra lỗi khi đăng nhập.
- Đã đăng nhập: khi người dùng đã đăng nhập.
Bạn có thể lập mô hình các trạng thái này dưới dạng một lớp kín. ViewModel
hiển thị trạng thái là
State
, đặt trạng thái ban đầu và cập nhật trạng thái nếu cần. Chiến lược phát hành đĩa đơn
ViewModel
cũng xử lý sự kiện đăng nhập bằng cách hiển thị một phương thức onSignIn()
.
class MyViewModel : ViewModel() { private val _uiState = mutableStateOf<UiState>(UiState.SignedOut) val uiState: State<UiState> get() = _uiState // ... }
Ngoài API mutableStateOf
, Compose cung cấp các tiện ích cho LiveData
, Flow
và Observable
để đăng ký làm trình nghe và đại diện cho giá trị của một trạng thái.
class MyViewModel : ViewModel() { private val _uiState = MutableLiveData<UiState>(UiState.SignedOut) val uiState: LiveData<UiState> get() = _uiState // ... } @Composable fun MyComposable(viewModel: MyViewModel) { val uiState = viewModel.uiState.observeAsState() // ... }
Tìm hiểu thêm
Để tìm hiểu thêm về cấu trúc trong Jetpack Compose, hãy tham khảo các tài nguyên sau:
Mẫu
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Trạng thái và Jetpack Compose
- Lưu trạng thái giao diện người dùng trong Compose
- Xử lý hoạt động đầu vào của người dùng