Lưu trạng thái giao diện người dùng trong Compose

Tuỳ thuộc vào vị trí mà trạng thái được chuyển lên và logic bắt buộc, bạn có thể dùng các API khác nhau để lưu trữ và khôi phục trạng thái giao diện người dùng. Mỗi ứng dụng dùng một tổ hợp API để đạt được mục tiêu này một cách hiệu quả nhất.

Mọi ứng dụng Android đều có thể mất trạng thái giao diện người dùng do việc tạo lại quá trình hoặc hoạt động. Việc mất trạng thái này có thể xảy ra do các sự kiện sau:

Việc duy trì trạng thái sau các sự kiện này là cần thiết để mang lại trải nghiệm người dùng tích cực. Việc chọn trạng thái để duy trì tuỳ thuộc vào luồng người dùng riêng biệt của ứng dụng. Tốt nhất là bạn nên giữ nguyên trạng thái liên quan đến thao tác và hoạt động đầu vào của người dùng. Ví dụ: vị trí thanh cuộn của danh sách, mã nhận dạng của mục mà người dùng muốn biết thêm thông tin chi tiết, tiến trình lựa chọn tuỳ chọn của người dùng hoặc dữ liệu đầu vào trong các trường văn bản.

Trang này tóm tắt các API dùng để lưu trữ trạng thái giao diện người dùng, tuỳ thuộc vào vị trí trạng thái được chuyển lên và logic cần trạng thái này.

Logic giao diện người dùng

Nếu trạng thái được chuyển lên trong giao diện người dùng, trong các hàm có khả năng kết hợp hoặc các lớp phần tử giữ trạng thái thuần tuý trong phạm vi Thành phần kết hợp, thì bạn có thể dùng rememberSaveable để giữ lại trạng thái giữa việc tạo lại quá trình và hoạt động.

Trong đoạn mã sau, rememberSaveable được dùng để lưu trữ trạng thái boolean duy nhất của thành phần trên giao diện người dùng:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Hình 1. Bong bóng tin nhắn trò chuyện mở rộng/thu gọn khi bạn nhấn vào.

showDetails là biến boolean dùng để lưu trữ giá trị xem bong bóng trò chuyện đang ở trạng thái thu gọn hoặc mở rộng.

rememberSaveable lưu trữ trạng thái của thành phần trên giao diện người dùng trong Bundle thông qua cơ chế trạng thái của thực thể đã lưu.

Hàm này có thể tự động lưu trữ các kiểu dữ liệu nguyên thuỷ vào gói. Nếu trạng thái của bạn được giữ trong một kiểu không phải là kiểu dữ liệu nguyên thuỷ, ví dụ như lớp dữ liệu, bạn có thể dùng các cơ chế lưu trữ khác, chẳng hạn như dùng chú giải Parcelize, dùng API Compose như listSavermapSaver hoặc triển khai một lớp trình lưu trữ tuỳ chỉnh mở rộng lớp Saver thời gian chạy trong Compose. Xem tài liệu về Các cách lưu trữ trạng thái để tìm hiểu thêm về các phương thức này.

Trong đoạn mã sau, API Compose rememberLazyListState dùng rememberSaveable để lưu trữ LazyListState, trong đó có chứa trạng thái cuộn của LazyColumn hoặc LazyRow. Phương thức này sử dụng LazyListState.Saver. Đây là một trình lưu trữ tuỳ chỉnh có thể lưu trữ và khôi phục trạng thái cuộn. Sau khi tạo lại một hoạt động hoặc quy trình (ví dụ: sau khi có sự thay đổi cấu hình như thay đổi hướng của thiết bị), trạng thái cuộn sẽ được giữ nguyên.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Phương pháp hay nhất

rememberSaveable sử dụng Bundle để lưu trữ trạng thái của giao diện người dùng (trạng thái này được chia sẻ bởi các API khác, vốn cũng có thể ghi vào trạng thái này), ví dụ như các lệnh gọi onSaveInstanceState() trong hoạt động của bạn. Tuy nhiên, kích thước của Bundle này bị hạn chế và việc lưu trữ các đối tượng lớn có thể dẫn đến các ngoại lệ TransactionTooLarge trong thời gian chạy. Điều này có thể đặc biệt rắc rối trong các ứng dụng dùng kiến trúc hoạt động đơn (single Activity) vì các ứng dụng này dùng cùng một Bundle trong ứng dụng.

Để tránh loại sự cố này, bạn không nên lưu trữ các đối tượng vừa lớn lại vừa phức tạp hoặc không nên lưu trữ danh sách đối tượng trong gói.

Thay vào đó, hãy lưu trữ trạng thái tối thiểu cần thiết, chẳng hạn như mã nhận dạng hoặc khoá, đồng thời sử dụng các trạng thái này để uỷ quyền khôi phục thêm trạng thái giao diện người dùng phức tạp cho các cơ chế khác, chẳng hạn như bộ nhớ liên tục.

Các lựa chọn thiết kế này phụ thuộc vào các trường hợp sử dụng cụ thể của ứng dụng và hành vi mà người dùng mong đợi.

Xác minh quá trình khôi phục trạng thái

Bạn có thể xác minh rằng trạng thái được lưu trữ bằng rememberSaveable ở các thành phần trong Compose được khôi phục đúng cách khi hoạt động hoặc quy trình được tạo lại. Có một số API cho mục đích này, chẳng hạn như StateRestorationTester. Hãy xem tài liệu về Kiểm thử để tìm hiểu thêm.

Logic nghiệp vụ

Nếu trạng thái của thành phần trên giao diện người dùng được chuyển lên ViewModel vì đây là trạng thái được logic kinh doanh yêu cầu, thì bạn có thể dùng các API của ViewModel.

Một trong những lợi ích chính của việc dùng ViewModel trong ứng dụng Android là giúp xử lý miễn phí các thay đổi về cấu hình. Khi có thay đổi về cấu hình cũng như khi hoạt động bị huỷ bỏ và được tạo lại, trạng thái giao diện người dùng được chuyển lên ViewModel sẽ được lưu trong bộ nhớ. Sau khi tạo lại, thực thể ViewModel cũ sẽ được đính kèm vào thực thể hoạt động mới.

Tuy nhiên, thực thể ViewModel không vượt qua được sự kiện bị buộc tắt do hệ thống gây ra. Để trạng thái giao diện người dùng vượt qua được sự kiện này, hãy sử dụng mô-đun Trạng thái đã lưu cho ViewModel, trong đó có chứa API SavedStateHandle.

Phương pháp hay nhất

SavedStateHandle cũng sử dụng cơ chế Bundle để lưu trữ trạng thái giao diện người dùng. Vì vậy, bạn chỉ nên sử dụng cơ chế này để lưu trữ trạng thái của thành phần trên giao diện người dùng đơn giản.

Bạn không nên lưu trữ trạng thái giao diện người dùng trên màn hình (được tạo bằng cách áp dụng các quy tắc nghiệp vụ và truy cập các lớp của ứng dụng ngoài giao diện người dùng) vào SavedStateHandle do độ phức tạp và kích thước của ứng dụng. Bạn có thể dùng các cơ chế khác nhau để lưu trữ dữ liệu phức tạp hoặc dữ liệu có kích thước lớn, như bộ nhớ cục bộ liên tục. Sau khi tạo lại quy trình, màn hình sẽ được tạo lại bằng trạng thái tạm thời đã khôi phục được lưu trữ trong SavedStateHandle (nếu có) và trạng thái giao diện người dùng trên màn hình được tạo lại từ lớp dữ liệu.

Các API SavedStateHandle

SavedStateHandle có nhiều API để lưu trữ trạng thái của thành phần trên giao diện người dùng, đáng chú ý nhất là:

Compose State saveable()
StateFlow getStateFlow()

Compose State

Sử dụng API saveable của SavedStateHandle để đọc và ghi trạng thái của thành phần trên giao diện người dùng dưới dạng MutableState, để đảm bảo API này vượt qua được quá trình tạo lại quá trình và hoạt động với quá trình thiết lập mã ở mức tối thiểu.

API saveable hỗ trợ các loại chính ngay từ đầu và nhận tham số stateSaver để sử dụng trình lưu tuỳ chỉnh, chẳng hạn như rememberSaveable().

Trong đoạn mã sau, message lưu trữ các loại dữ liệu từ hoạt động đầu vào của người dùng vào TextField:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

Xem tài liệu SavedStateHandle để biết thêm thông tin về cách sử dụng API saveable.

StateFlow

Sử dụng getStateFlow() để lưu trữ trạng thái của thành phần trên giao diện người dùng và sử dụng trạng thái này dưới dạng luồng từ SavedStateHandle. StateFlow ở chế độ chỉ có thể đọc và API yêu cầu bạn chỉ định khoá để có thể thay thế flow (luồng) giúp tạo giá trị mới. Với khoá đã định cấu hình, bạn có thể truy xuất StateFlow và thu thập giá trị mới nhất.

Trong đoạn mã sau, savedFilterType là biến StateFlow giúp lưu trữ loại bộ lọc áp dụng cho danh sách kênh trò chuyện trong ứng dụng trò chuyện:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

Mỗi khi người dùng chọn một loại bộ lọc mới, setFiltering sẽ được gọi. Thao tác này sẽ lưu một giá trị mới trong SavedStateHandle được lưu trữ bằng khoá _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType là một flow (luồng) tạo giá trị mới nhất được lưu trữ vào khoá. filteredChannels được đăng ký với flow (luồng) để thực hiện việc lọc kênh.

Xem tài liệu SavedStateHandle để biết thêm thông tin về API getStateFlow().

Tóm tắt

Bảng sau đây tóm tắt các API được đề cập trong phần này và thời điểm sử dụng từng API để lưu trạng thái của giao diện người dùng:

Sự kiện Logic giao diện người dùng Logic nghiệp vụ trong ViewModel
Các thay đổi về cấu hình rememberSaveable Tự động
Sự kiện bị buộc tắt do hệ thống gây ra rememberSaveable SavedStateHandle

API cần sử dụng phụ thuộc vào vị trí lưu giữ trạng thái và logic mà API đó yêu cầu. Đối với trạng thái dùng trong logic giao diện người dùng, hãy sử dụng rememberSaveable. Đối với trạng thái dùng trong logic nghiệp vụ, nếu bạn giữ trạng thái đó trong ViewModel, hãy lưu bằng cách dùng SavedStateHandle.

Bạn nên dùng các API gói (rememberSaveableSavedStateHandle) để lưu trữ một lượng nhỏ trạng thái giao diện người dùng. Đây là dữ liệu tối thiểu cần thiết để khôi phục giao diện người dùng về trạng thái trước đó, cùng với các cơ chế lưu trữ khác. Ví dụ: nếu lưu trữ mã nhận dạng của một hồ sơ mà người dùng đang xem trong gói, bạn có thể tìm nạp nhiều dữ liệu, chẳng hạn như thông tin chi tiết về hồ sơ, từ lớp dữ liệu.

Để biết thêm thông tin về các cách lưu trạng thái giao diện người dùng, hãy xem tài liệu chung về cách lưu trạng thái giao diện người dùng và trang lớp dữ liệu của hướng dẫn cấu trúc.