Sự kiện giao diện người dùng là các hành động cần được giao diện người dùng hoặc ViewModel xử lý trong lớp giao diện người dùng. Loại sự kiện phổ biến nhất là sự kiện của người dùng. Người dùng tạo sự kiện của người dùng bằng cách tương tác với ứng dụng, ví dụ: bằng cách nhấn vào màn hình hoặc tạo cử chỉ. Sau đó, giao diện người dùng sẽ xử lý các sự kiện này bằng các lệnh gọi lại, chẳng hạn như lambda được xác định trên các thành phần kết hợp khác nhau.
ViewModel thường chịu trách nhiệm xử lý logic kinh doanh của một sự kiện của
người dùng cụ thể—ví dụ: người dùng nhấp vào một nút để làm mới một số
dữ liệu. Thông thường, ViewModel xử lý vấn đề này bằng cách hiển thị các hàm mà giao diện người dùng có thể
gọi. Sự kiện của người dùng cũng có thể có logic của hoạt động trên giao diện người dùng mà giao diện người dùng có thể xử lý
trực tiếp—ví dụ: chuyển đến một màn hình khác hoặc hiển thị
Snackbar.
Mặc dù logic nghiệp vụ vẫn giữ nguyên cho cùng một ứng dụng trên nhiều kiểu dáng hoặc nền tảng thiết bị di động, nhưng logic của hành vi trên giao diện người dùng là các nội dung chi tiết triển khai có thể khác nhau giữa những trường hợp đó. Trang lớp giao diện người dùng xác định các loại logic sau:
- Logic nghiệp vụ cho biết việc nên làm với những thay đổi về trạng thái, chẳng hạn như thanh toán hoặc lưu trữ các lựa chọn ưu tiên của người dùng. Các lớp miền và lớp dữ liệu thường xử lý logic này. Xuyên suốt hướng dẫn này, lớp Các thành phần cấu trúc ViewModel được dùng làm giải pháp quan trọng cho các lớp xử lý logic kinh doanh.
- Logic của hoạt động giao diện người dùng hoặc logic giao diện người dùng đề cập đến cách hiển thị các thay đổi về trạng thái—ví dụ: logic điều hướng hoặc cách hiển thị thông báo cho người dùng. Giao diện người dùng xử lý logic này.
Cây quyết định sự kiện trên giao diện người dùng
Sơ đồ dưới đây là hình ảnh cây quyết định để tìm phương pháp tốt nhất cho việc xử lý một trường hợp sử dụng sự kiện cụ thể. Phần còn lại của hướng dẫn này sẽ giải thích chi tiết về các cách tiếp cận này.
Xử lý sự kiện của người dùng
Giao diện người dùng có thể trực tiếp xử lý các sự kiện của người dùng nếu những sự kiện đó liên quan đến việc sửa đổi trạng thái của một thành phần trên giao diện người dùng—ví dụ: trạng thái của một mục có thể mở rộng. Nếu sự kiện yêu cầu thực hiện logic kinh doanh, chẳng hạn như làm mới dữ liệu trên màn hình, thì sự kiện này sẽ do ViewModel xử lý.
Ví dụ sau đây cho thấy cách các nút khác nhau được dùng để mở rộng thành phần trên giao diện người dùng (logic giao dịch người dùng) và làm mới dữ liệu trên màn hình (logic kinh doanh):
@Composable
fun LatestNewsScreen(viewModel: LatestNewsViewModel = viewModel()) {
// State of whether more details should be shown
var expanded by remember { mutableStateOf(false) }
Column {
Text("Some text")
if (expanded) {
Text("More details")
}
Button(
// The expand details event is processed by the UI that
// modifies this composable's internal state.
onClick = { expanded = !expanded }
) {
val expandText = if (expanded) "Collapse" else "Expand"
Text("$expandText details")
}
// The refresh event is processed by the ViewModel that is in charge
// of the UI's business logic.
Button(onClick = { viewModel.refreshNews() }) {
Text("Refresh data")
}
}
}
Sự kiện người dùng trong danh sách lazy
Nếu hành động được tạo tiếp theo trong cây giao diện người dùng, chẳng hạn như trong mục LazyColumn, thì ViewModel vẫn phải là mã xử lý các sự kiện của người dùng.
Ví dụ: hãy xem xét một danh sách các mục có thể nhấp. Đừng truyền thực thể ViewModel xuống thành phần kết hợp danh sách (MyList), vì điều này sẽ liên kết chặt chẽ thành phần giao diện người dùng với thông tin triển khai chi tiết.
Thay vào đó, hãy hiển thị sự kiện này dưới dạng tham số hàm lambda trong thành phần kết hợp. Điều này cho phép danh sách kích hoạt sự kiện mà không cần biết ai hoặc cách xử lý sự kiện đó.
data class MyItem(val id: Int)
@Composable
fun MyList(
items: List<String>,
onItemClick: (MyItem) -> Unit
) {
Card {
LazyColumn {
itemsIndexed(items) { index, string ->
ListItem(
modifier = Modifier.clickable {
onItemClick(MyItem(index))
},
headlineContent = {
Text(text = string)
}
)
}
}
}
}
Theo phương pháp này, thành phần kết hợp MyList chỉ hoạt động với dữ liệu mà thành phần này hiển thị và các sự kiện mà thành phần này hiển thị. Nó không có quyền truy cập vào ViewModel. Sự kiện này được lưu trữ và truyền đến một ViewModel trong một thành phần kết hợp trước đó.
Để biết thêm thông tin về việc xử lý sự kiện, hãy xem bài viết Sự kiện trong Compose.
Quy ước đặt tên cho các hàm sự kiện của người dùng và trình xử lý sự kiện
Trong hướng dẫn này, các hàm ViewModel xử lý các sự kiện của người dùng được đặt tên bằng một
động từ dựa trên hành động mà các hàm đó xử lý–ví dụ: validateInput() hoặc
login().
Các trình xử lý sự kiện trong Compose tuân theo một quy ước đặt tên tiêu chuẩn để làm rõ luồng dữ liệu:
- Tên tham số:
on+Verb+Target(ví dụ:onExpandClickedhoặconValueChange). - Biểu thức lambda: Khi gọi thành phần kết hợp, lambda thường chỉ là việc triển khai sự kiện đó.
Xử lý sự kiện ViewModel
Các hành động trên giao diện người dùng bắt nguồn từ ViewModel–sự kiện ViewModel phải–luôn dẫn đến việc cập nhật trạng thái giao diện người dùng. Điều này tuân thủ các nguyên tắc về Luồng dữ liệu một chiều. Điều này giúp các sự kiện có thể tái tạo sau khi thay đổi cấu hình và đảm bảo rằng các hành động trên giao diện người dùng sẽ không bị mất. Nếu muốn, bạn cũng có thể tạo các sự kiện có thể tái tạo sau quá trình bị gián đoạn nếu sử dụng mô-đun trạng thái đã lưu.
Việc liên kết các hành động trên giao diện người dùng với trạng thái giao diện người dùng không phải lúc nào cũng là một quy trình đơn giản, nhưng quy trình này sẽ dẫn đến logic đơn giản hơn. Ví dụ: Quy trình tư duy của bạn không nên kết thúc bằng việc xác định cách giao diện người dùng điều hướng trên một màn hình cụ thể. Bạn cần suy nghĩ thêm và xem xét cách thể hiện luồng người dùng đó trong trạng thái giao diện người dùng. Nói cách khác, đừng nghĩ về những hành động mà giao diện người dùng cần thực hiện, hãy nghĩ về các hành động đó ảnh hưởng như thế nào đến trạng thái giao diện người dùng.
Ví dụ: hãy xem xét trường hợp màn hình đăng nhập. Bạn có thể lập mô hình trạng thái giao diện người dùng của màn hình này như sau:
data class LoginUiState(
val isLoginInProgress: Boolean = false,
val errorMessage: String? = null,
val isUserLoggedIn: Boolean = false
)
Màn hình đăng nhập phản ứng với các thay đổi về trạng thái giao diện người dùng.
class LoginViewModel : ViewModel() {
var uiState by mutableStateOf(LoginUiState())
fun tryLogin(username: String, password: String) {
viewModelScope.launch {
// Emit a new state indicating that login is in progress
uiState = uiState.copy(isLoginInProgress = true)
uiState = if (login(username, password)) {
// Emit a new state indicating that login was successful
uiState.copy(isLoginInProgress = false, isUserLoggedIn = true)
} else {
// Emit a new state with the error message
LoginUiState(isLoginInProgress = false, errorMessage = "Login failed")
}
}
}
private suspend fun login(username: String, password: String): Boolean {
delay(1000)
return (username == "Hello" && password == "World!")
}
}
@Composable
fun LoginScreen(viewModel: LoginViewModel, onSuccessfulLogin: () -> Unit) {
val uiState = viewModel.uiState
LaunchedEffect(uiState) {
if (uiState.isUserLoggedIn) {
onSuccessfulLogin()
}
}
if (uiState.isLoginInProgress) {
CircularProgressIndicator()
} else {
LoginForm(
onLoginAttempt = { username, password ->
viewModel.tryLogin(username, password)
},
errorMessage = uiState.errorMessage
)
}
}
Việc sử dụng các sự kiện có thể kích hoạt cập nhật trạng thái
Việc sử dụng một số sự kiện ViewModel trong giao diện người dùng có thể dẫn đến các lần cập nhật trạng thái giao diện người dùng khác. Ví dụ: khi hiển thị các thông báo tạm thời trên màn hình để người dùng biết rằng đã có sự kiện xảy ra, thì giao diện người dùng cần phải thông báo cho ViewModel để kích hoạt một nội dung cập nhật trạng thái khác khi thông báo đã hiển thị trên màn hình. Sự kiện xảy ra khi người dùng sử dụng thông báo (bằng cách loại bỏ thông báo hoặc sau khi hết thời gian chờ) có thể được coi là "dữ liệu do người dùng nhập" và do đó, ViewModel cần biết điều đó. Trong trường hợp này, trạng thái giao diện người dùng có thể được lập mô hình như sau:
// Models the UI state for the Latest news screen.
data class LatestNewsUiState(
val news: List<News> = emptyList(),
val isLoading: Boolean = false,
val userMessage: String? = null
)
ViewModel sẽ cập nhật trạng thái giao diện người dùng như sau khi logic nghiệp vụ yêu cầu hiển thị một thông báo tạm thời mới cho người dùng:
class LatestNewsViewModel(/* ... */) : ViewModel() {
var uiState by mutableStateOf(LatestNewsUiState())
private set
fun refreshNews() {
viewModelScope.launch {
// If there isn't internet connection, show a new message on the screen.
if (!internetConnection()) {
uiState = uiState.copy(userMessage = "No Internet connection")
return@launch
}
// Do something else.
}
}
fun userMessageShown() {
uiState = uiState.copy(userMessage = null)
}
}
ViewModel không cần phải biết cách giao diện người dùng hiển thị thông báo trên màn hình mà chỉ biết rằng cần phải hiển thị một thông báo cho người dùng. Sau khi thông báo tạm thời hiển thị, giao diện người dùng cần thông báo cho ViewModel về điều đó, dẫn đến việc cập nhật trạng thái giao diện người dùng khác để xoá thuộc tính userMessage:
@Composable
fun LatestNewsScreen(
snackbarHostState: SnackbarHostState,
viewModel: LatestNewsViewModel = viewModel(),
) {
// Rest of the UI content.
// If there are user messages to show on the screen,
// show it and notify the ViewModel.
viewModel.uiState.userMessage?.let { userMessage ->
LaunchedEffect(userMessage) {
snackbarHostState.showSnackbar(userMessage)
// Once the message is displayed and dismissed, notify the ViewModel.
viewModel.userMessageShown()
}
}
}
Mặc dù thông báo chỉ mang tính tạm thời, nhưng trạng thái giao diện người dùng phản ánh chính xác nội dung hiển thị trên màn hình tại mỗi thời điểm. Thông báo cho người dùng sẽ hiển thị hoặc không.
Sự kiện điều hướng
Phần Việc sử dụng các sự kiện có thể kích hoạt nội dung cập nhật trạng thái nêu chi tiết cách bạn dùng trạng thái giao diện người dùng để hiển thị thông báo cho người dùng trên màn hình. Sự kiện điều hướng cũng là một loại sự kiện phổ biến trong ứng dụng Android.
Nếu sự kiện đó được kích hoạt trong giao diện người dùng do người dùng nhấn vào một nút, thì giao diện người dùng sẽ xử lý vấn đề đó bằng cách hiển thị sự kiện cho thành phần kết hợp của phương thức gọi.
@Composable
fun LoginScreen(
onHelp: () -> Unit, // Caller navigates to the help screen
viewModel: LoginViewModel = viewModel()
) {
// Rest of the UI
Button(
onClick = dropUnlessResumed { onHelp() }
) {
Text("Get help")
}
}
dropUnlessResumed là một phần của thư viện Vòng đời và cho phép bạn chạy hàm onHelp chỉ khi vòng đời đạt ít nhất là RESUMED.
Nếu việc nhập dữ liệu yêu cầu xác thực logic kinh doanh trước khi điều hướng, ViewModel cần hiển thị trạng thái đó cho giao diện người dùng. Giao diện người dùng sẽ phản ứng với sự thay đổi về trạng thái đó và điều hướng cho phù hợp. Phần Xử lý các sự kiện ViewModel trình bày về trường hợp sử dụng này. Dưới đây là mã tương tự:
@Composable
fun LoginScreen(
onUserLogIn: () -> Unit, // Caller navigates to the right screen
viewModel: LoginViewModel = viewModel()
) {
Button(
onClick = {
// ViewModel validation is triggered
viewModel.tryLogin()
}
) {
Text("Log in")
}
// Rest of the UI
val lifecycle = LocalLifecycleOwner.current.lifecycle
val currentOnUserLogIn by rememberUpdatedState(onUserLogIn)
LaunchedEffect(viewModel, lifecycle) {
// Whenever the uiState changes, check if the user is logged in and
// call the `onUserLogin` event when `lifecycle` is at least STARTED
snapshotFlow { viewModel.uiState }
.filter { it.isUserLoggedIn }
.flowWithLifecycle(lifecycle)
.collect {
currentOnUserLogIn()
}
}
}
Trong ví dụ trên, ứng dụng hoạt động như mong đợi vì đích đến hiện tại, Login (Đăng nhập), sẽ không được lưu giữ trong ngăn xếp lui. Người dùng không thể quay lại nếu nhấn vào nút Quay lại. Tuy nhiên, trong trường hợp điều đó có thể xảy ra, giải pháp sẽ yêu cầu thêm logic.
Sự kiện điều hướng khi đích đến được lưu giữ trong ngăn xếp lui
Khi ViewModel đặt trạng thái để tạo sự kiện điều hướng từ màn hình A sang màn hình B và màn hình A được giữ lại trong ngăn xếp lui điều hướng, bạn có thể cần thêm logic để không tự động chuyển sang B. Để triển khai việc này, bạn cần có trạng thái bổ sung để cho biết giao diện người dùng có nên chuyển sang màn hình khác hay không. Thông thường, trạng thái đó được lưu giữ trong giao diện người dùng vì logic điều hướng liên quan đến giao diện người dùng, chứ không phải ViewModel. Để minh hoạ điều này, hãy xem xét trường hợp sử dụng sau.
Giả sử bạn đang thực hiện quy trình đăng ký trong ứng dụng. Trên màn hình xác thực ngày sinh, khi người dùng nhập một ngày, ViewModel sẽ xác thực ngày đó khi người dùng nhấn vào nút "Tiếp tục". ViewModel uỷ quyền logic xác thực cho lớp dữ liệu. Nếu ngày này hợp lệ, người dùng sẽ chuyển sang màn hình tiếp theo. Ngoài ra, người dùng cũng có thể quay lại và chuyển giữa các màn hình đăng ký nếu muốn thay đổi một số dữ liệu. Do đó, tất cả các đích đến trong quy trình đăng ký đều được lưu giữ trong cùng một ngăn xếp lui. Với các yêu cầu này, bạn có thể triển khai màn hình này như sau:
class DobValidationViewModel(/* ... */) : ViewModel() {
var uiState by mutableStateOf(DobValidationUiState())
private set
}
@Composable
fun DobValidationScreen(
onNavigateToNextScreen: () -> Unit, // Caller navigates to the right screen
viewModel: DobValidationViewModel = viewModel()
) {
// TextField that updates the ViewModel when a date of birth is selected
var validationInProgress by rememberSaveable { mutableStateOf(false) }
Button(
onClick = {
viewModel.validateInput()
validationInProgress = true
}
) {
Text("Continue")
}
// Rest of the UI
/*
* The following code implements the requirement of advancing automatically
* to the next screen when a valid date of birth has been introduced
* and the user wanted to continue with the registration process.
*/
if (validationInProgress) {
val lifecycle = LocalLifecycleOwner.current.lifecycle
val currentNavigateToNextScreen by rememberUpdatedState(onNavigateToNextScreen)
LaunchedEffect(viewModel, lifecycle) {
// If the date of birth is valid and the validation is in progress,
// navigate to the next screen when `lifecycle` is at least STARTED,
// which is the default Lifecycle.State for the `flowWithLifecycle` operator.
snapshotFlow { viewModel.uiState }
.filter { it.isDobValid }
.flowWithLifecycle(lifecycle)
.collect {
validationInProgress = false
currentNavigateToNextScreen()
}
}
}
}
Ngày xác thực ngày sinh là logic kinh doanh do ViewModel chịu trách nhiệm. Trong hầu hết các trường hợp, ViewModel sẽ uỷ quyền logic đó cho lớp dữ liệu. Logic để điều hướng người dùng đến màn hình tiếp theo là logic giao diện người dùng vì các yêu cầu này có thể thay đổi tuỳ thuộc vào cấu hình giao diện người dùng. Ví dụ: bạn có thể không muốn tự động chuyển sang một màn hình khác trong máy tính bảng nếu đang hiển thị nhiều bước đăng ký cùng một lúc. Biến validationInProgress trong mã ở trên sẽ triển khai chức năng này và xử lý xem giao diện người dùng có tự động điều hướng bất cứ khi nào ngày sinh hợp lệ hay không và người dùng muốn chuyển sang bước đăng ký tiếp theo.
Các trường hợp sử dụng khác
Nếu bạn cho rằng các nội dung cập nhật trạng thái giao diện người dùng không thể giải quyết trường hợp sử dụng sự kiện giao diện người dùng của bạn, thì bạn có thể cần xem xét lại cách dữ liệu lưu chuyển trong ứng dụng của bạn. Hãy xem xét các nguyên tắc sau:
- Mỗi lớp chỉ nên làm những việc thuộc trách nhiệm của nó, chứ không phải nhiều việc hơn. Giao diện người dùng chịu trách nhiệm về logic hoạt động trên một màn hình cụ thể như lệnh gọi điều hướng, nhấp vào sự kiện và yêu cầu quyền truy cập. ViewModel chứa logic kinh doanh và chuyển đổi kết quả từ các lớp thấp hơn trong hệ phân cấp sang trạng thái giao diện người dùng.
- Hãy nghĩ về nguồn gốc của sự kiện. Thực hiện theo cây quyết định được trình bày ở đầu hướng dẫn này và thiết lập sao cho mỗi lớp chỉ xử lý những việc thuộc trách nhiệm của nó. Ví dụ: nếu sự kiện xuất phát từ giao diện người dùng và dẫn đến một sự kiện điều hướng, thì sự kiện đó phải được xử lý trong giao diện người dùng. Một số logic có thể được uỷ quyền cho ViewModel, nhưng bạn không thể uỷ quyền hoàn toàn việc xử lý sự kiện cho ViewModel.
- Nếu có nhiều người dùng và bạn lo ngại rằng sự kiện này sẽ được sử dụng nhiều lần, thì bạn có thể phải xem xét lại cấu trúc ứng dụng của mình. Việc có nhiều người dùng đồng thời dẫn đến việc cực kỳ khó đảm bảo hợp đồng được phân phối chính xác một lần, do đó, số lượng hoạt động phức tạp và sẽ bùng nổ. Nếu bạn gặp sự cố này, hãy xem xét việc đẩy các mối quan tâm đó lên trên cây giao diện người dùng; bạn có thể cần một thực thể khác ở cấp cao hơn trong hệ thống phân cấp.
- Hãy nghĩ về thời điểm cần sử dụng trạng thái đó. Trong một số trường hợp,
bạn có thể không muốn tiếp tục sử dụng trạng thái khi ứng dụng chạy ở
chế độ nền, ví dụ: hiển thị một
Toast. Trong những trường hợp đó, hãy cân nhắc sử dụng trạng thái khi giao diện người dùng chạy trên nền trước.
Mẫu
Các mẫu sau đây của Google minh hoạ các sự kiện trên giao diện người dùng trong lớp giao diện người dùng. Hãy khám phá những mẫu đó để xem hướng dẫn này trong thực tế:
Tài nguyên khác
Để biết thêm thông tin về các sự kiện trên giao diện người dùng, hãy xem các tài nguyên bổ sung sau đây:
Lớp học lập trình
Tài liệu
Xem nội dung
Đề xuất cho bạn
- Lưu ý: văn bản có đường liên kết sẽ hiện khi JavaScript tắt
- Lớp giao diện người dùng
- Phần tử giữ trạng thái và trạng thái giao diện người dùng {:#mad-arch}
- Hướng dẫn về cấu trúc ứng dụng