Thiết kế giao diện người dùng trong Compose

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.

Hình 1. Luồng dữ liệu một chiều.

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ặc LiveData.

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 TextFieldString, 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 titlesubtitle, 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 titlesubtitle 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 ViewModelmutableStateOf, 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ặc LiveData.
  • 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, FlowObservable để đă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