Phần tử giữ trạng thái và trạng thái giao diện người dùng

Hướng dẫn về lớp giao diện người dùng sẽ thảo luận về luồng dữ liệu một chiều (UDF) như một phương tiện để tạo và quản lý Trạng thái giao diện người dùng cho lớp giao diện người dùng.

Dữ liệu di chuyển một chiều từ lớp dữ liệu đến giao diện người dùng.
Hình 1: Luồng dữ liệu một chiều

Hướng dẫn này cũng nêu bật lợi ích của việc uỷ quyền quản lý UDF cho một lớp đặc biệt được gọi là phần tử giữ trạng thái. Bạn có thể triển khai phần tử giữ trạng thái thông qua ViewModel hoặc một lớp thuần tuý. Tài liệu này xem xét kỹ hơn các phần tử giữ trạng thái và vai trò của chúng trong lớp giao diện người dùng.

Ở cuối tài liệu này, bạn sẽ hiểu cách quản lý trạng thái ứng dụng trong lớp giao diện người dùng; đó là quy trình tạo trạng thái giao diện người dùng. Bạn sẽ hiểu và nạp thêm kiến thức dưới đây:

  • Hiểu rõ các loại trạng thái giao diện người dùng tồn tại trong lớp giao diện người dùng.
  • Nắm được nhiều loại logic hoạt động trên các trạng thái giao diện người dùng đó trong lớp giao diện người dùng
  • Biết cách chọn phương thức triển khai phù hợp một phần tử giữ trạng thái, chẳng hạn như ViewModel hoặc một lớp thuần tuý.

Các thành phần của quy trình tạo trạng thái giao diện người dùng

Trạng thái giao diện người dùng và logic tạo ra trạng thái này xác định lớp giao diện người dùng.

Trạng thái giao diện người dùng

Trạng thái giao diện người dùng là thuộc tính mô tả giao diện người dùng. Có hai loại trạng thái giao diện người dùng:

  • Trạng thái giao diện người dùng màn hìnhnội dung bạn cần hiển thị trên màn hình. Ví dụ như một lớp NewsUiState có thể chứa các bài viết tin tức và những thông tin cần thiết khác để kết xuất giao diện người dùng. 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.
  • Trạng thái thành phần giao diện người dùng đề cập đến các thuộc tính hàm nội tại với các thành phần giao diện người dùng ảnh hưởng đến cách hiển thị các thành phần đó. Một phần tử trên giao diện người dùng có thể được hiện/ẩn cũng như chữ của phần từ này có thể có phông, kích thước hoặc màu sắc nhất định. Trong Android Views, lớp View tự quản lý trạng thái này vì nó vốn có trạng thái, hiển thị các phương thức để sửa đổi hoặc truy vấn trạng thái của nó. Ví dụ như phương thức getset của lớp TextView cho văn bản của lớp đó. Trong Jetpack Compose, trạng thái nằm bên ngoài thành phần kết hợp và bạn thậm chí có thể chuyển nó lên trên từ vùng lân cận của thành phần kết hợp vào hàm có thể kết hợp đang gọi hoặc phần tử giữ trạng thái. Ví dụ như ScaffoldState cho thành phần kết hợp Scaffold.

Logic

Trạng thái giao diện người dùng không phải là một thuộc tính tĩnh, vì dữ liệu ứng dụng và các sự kiện của người dùng khiến trạng thái giao diện người dùng thay đổi theo thời gian. Logic xác định các chi tiết cụ thể của thay đổi, bao gồm cả những phần của trạng thái giao diện người dùng đã thay đổi, lý do thay đổi và thời điểm thay đổi.

Logic tạo ra trạng thái giao diện người dùng
Hình 2: Logic tạo ra trạng thái giao diện người dùng

Logic trong ứng dụng có thể là logic nghiệp vụ hoặc logic giao diện người dùng:

  • Logic nghiệp vụ là cách áp dụng các yêu cầu của sản phẩm cho dữ liệu ứng dụng. Ví dụ như đánh dấu một bài viết trong ứng dụng đọc tin tức khi người dùng nhấn nút. Logic này để lưu dấu trang vào một tệp hoặc cơ sở dữ liệu thường được đặt trong miền hoặc lớp dữ liệu. Phần tử giữ trạng thái thường uỷ quyền logic này cho các lớp đó bằng cách gọi phương thức mà các lớp này hiển thị.
  • Logic giao diện người dùng liên quan đến cách hiển thị trạng thái giao diện người dùng trên màn hình. Ví dụ như nhận được gợi ý phù hợp trên thanh tìm kiếm khi người dùng đã chọn một danh mục, cuộn đến một mục cụ thể trong danh sách hoặc logic điều hướng đến một màn hình cụ thể khi người dùng nhấp vào một nút.

Vòng đời của Android cũng như các loại trạng thái và logic của giao diện người dùng

Lớp giao diện người dùng có hai phần: một phần phụ thuộc và phần còn lại độc lập với vòng đời của giao diện người dùng (vòng đời UI). Việc phân tách này xác định các nguồn dữ liệu có sẵn cho từng phần, do đó yêu cầu nhiều loại trạng thái và logic của giao diện người dùng.

  • Phần độc lập với vòng đời giao diện người dùng: Phần này của lớp giao diện người dùng xử lý các lớp tạo dữ liệu của ứng dụng (lớp dữ liệu hoặc lớp miền) và được xác định theo logic nghiệp vụ. Vòng đời, thay đổi về cấu hình và hoạt động tạo lại Activity trong giao diện người dùng có thể ảnh hưởng nếu quy trình tạo trạng thái giao diện người dùng đang hoạt động, nhưng không ảnh hưởng đến tính hợp lệ của dữ liệu được tạo.
  • Phần phụ thuộc của vòng đời giao diện người dùng: Phần này của lớp giao diện người dùng xử lý logic giao diện người dùng, đồng thời chịu ảnh hưởng trực tiếp của các thay đổi về vòng đời hoặc cấu hình. Những thay đổi này trực tiếp ảnh hưởng đến tính hợp lệ của các nguồn dữ liệu được đọc trong đó, và do đó, trạng thái của nó chỉ có thể thay đổi khi vòng đời đang hoạt động. Ví dụ về vấn đề này bao gồm các quyền khi bắt đầu chạy và việc nhận tài nguyên phụ thuộc vào cấu hình, chẳng hạn như các chuỗi đã được bản địa hoá.

Thông tin ở trên có thể được tóm tắt qua bảng dưới đây:

Phần độc lập với Vòng đời UI Phần phụ thuộc vào Vòng đời UI
Logic nghiệp vụ Logic giao diện người dùng
Trạng thái giao diện người dùng trên màn hình

Quy trình tạo trạng thái giao diện người dùng

Quy trình tạo trạng thái giao diện người dùng là các bước được thực hiện để tạo trạng thái giao diện người dùng. Các bước này bao gồm việc áp dụng các loại logic được xác định trước đó, và hoàn toàn phụ thuộc vào nhu cầu của giao diện người dùng. Về phần độc lập và phần phụ thuộc với Vòng đời UI trong quy trình, một số giao diện người dùng có thể hưởng lợi từ cả hai phần hoặc một trong hai phần hoặc không phần nào cả.

Điều đó có nghĩa là các phép hoán vị sau của quy trình lớp giao diện người dùng hợp lệ:

  • Trạng thái giao diện người dùng do chính giao diện người dùng tạo ra và quản lý. Ví dụ: một bộ đếm cơ bản đơn giản và có thể sử dụng lại:

    @Composable
    fun Counter() {
        // The UI state is managed by the UI itself
        var count by remember { mutableStateOf(0) }
        Row {
            Button(onClick = { ++count }) {
                Text(text = "Increment")
            }
            Button(onClick = { --count }) {
                Text(text = "Decrement")
            }
        }
    }
    
  • Logic giao diện người dùng → giao diện người dùng. Ví dụ: hiện hoặc ẩn nút cho phép người dùng chuyển lên đầu danh sách.

    @Composable
    fun ContactsList(contacts: List<Contact>) {
        val listState = rememberLazyListState()
        val isAtTopOfList by remember {
            derivedStateOf {
                listState.firstVisibleItemIndex < 3
            }
        }
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Show or hide the button (UI logic) based on the list scroll position
        AnimatedVisibility(visible = !isAtTopOfList) {
            ScrollToTopButton()
        }
    }
    
  • Logic nghiệp vụ → giao diện người dùng. Một thành phần trên giao diện người dùng hiển thị ảnh của người dùng hiện tại trên màn hình.

    @Composable
    fun UserProfileScreen(viewModel: UserProfileViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
        // Call on the UserAvatar Composable to display the photo
        UserAvatar(picture = uiState.profilePicture)
    }
    
  • Logic nghiệp vụ → logic giao diện người dùng → giao diện người dùng. Một thành phần trên giao diện người dùng cuộn để hiển thị thông tin phù hợp trên màn hình cho một trạng thái giao diện người dùng nhất định.

    @Composable
    fun ContactsList(viewModel: ContactsViewModel = hiltViewModel()) {
        // Read screen UI state from the business logic state holder
        val uiState by viewModel.uiState.collectAsStateWithLifecycle()
        val contacts = uiState.contacts
        val deepLinkedContact = uiState.deepLinkedContact
    
        val listState = rememberLazyListState()
    
        // Create the LazyColumn with the lazyListState
        ...
    
        // Perform UI logic that depends on information from business logic
        if (deepLinkedContact != null && contacts.isNotEmpty()) {
            LaunchedEffect(listState, deepLinkedContact, contacts) {
                val deepLinkedContactIndex = contacts.indexOf(deepLinkedContact)
                if (deepLinkedContactIndex >= 0) {
                  // Scroll to deep linked item
                  listState.animateScrollToItem(deepLinkedContactIndex)
                }
            }
        }
    }
    

Trong trường hợp cả hai loại logic áp dụng cho quy trình tạo trạng thái giao diện người dùng, logic nghiệp vụ phải luôn được áp dụng trước logic giao diện người dùng. Việc cố gắng áp dụng logic nghiệp vụ sau logic giao diện người dùng sẽ ngụ ý logic nghiệp vụ đó phụ thuộc vào logic giao diện người dùng. Các phần sau đây trình bày lý do khiến đây là một vấn đề thông qua việc tìm hiểu sâu về các loại logic khác nhau và phần tử giữ trạng thái của các loại đó.

Dữ liệu di chuyển từ lớp tạo dữ liệu sang giao diện người dùng
Hình 3: Áp dụng logic trong lớp giao diện người dùng

Phần tử giữ trạng thái và trách nhiệm của chúng

Trách nhiệm của phần tử giữ trạng thái là lưu trữ trạng thái để ứng dụng có thể đọc. Trong trường hợp cần logic, logic đó đóng vai trò là bên trung gian cung cấp quyền truy cập vào các nguồn dữ liệu lưu trữ logic cần thiết. Qua đó, phần tử giữ trạng thái sẽ uỷ quyền logic cho nguồn dữ liệu thích hợp.

Điều này mang lại các lợi ích sau:

  • Giao diện người dùng đơn giản: Giao diện người dùng chỉ liên kết với trạng thái của nó.
  • Khả năng duy trì: Logic được xác định trong phần tử giữ trạng thái có thể lặp lại mà không cần thay đổi chính giao diện người dùng.
  • Khả năng thử nghiệm: Giao diện người dùng và logic tạo trạng thái có thể được kiểm thử một cách độc lập.
  • Tính dễ đọc: Người đọc mã có thể thấy rõ sự khác biệt giữa mã trình bày giao diện người dùng và mã tạo trạng thái giao diện người dùng.

Dù với bất kỳ kích thước hoặc phạm vi nào, mọi thành phần giao diện người dùng cũng đều có mối quan hệ 1:1 với phần tử giữ trạng thái tương ứng. Hơn nữa, phần tử giữ trạng thái phải có khả năng chấp nhận và xử lý mọi hành động của người dùng có thể dẫn đến sự thay đổi trạng thái trên giao diện người dùng, đồng thời phải tạo ra các thay đổi trạng thái tiếp theo.

Các loại phần tử giữ trạng thái

Tương tự như các kiểu trạng thái và logic của giao diện người dùng, có hai loại phần tử giữ trạng thái trong lớp giao diện người dùng được xác định theo mối quan hệ của chúng với vòng đời của giao diện người dùng:

  • phần tử giữ trạng thái logic nghiệp vụ.
  • Phần tử giữ trạng thái logic của giao diện người dùng.

Phần sau sẽ tìm hiểu kỹ hơn về các loại phần tử giữ trạng thái, bắt đầu từ phần tử giữ trạng thái logic nghiệp vụ.

Logic nghiệp vụ và phần tử giữ trạng thái của nó

Phần tử giữ trạng thái logic nghiệp vụ xử lý sự kiện của người dùng và biến đổi dữ liệu từ các lớp dữ liệu hoặc lớp miền sang trạng thái giao diện người dùng màn hình. Nhằm cung cấp trải nghiệm người dùng tối ưu khi xem xét vòng đời của Android và các thay đổi về cấu hình ứng dụng, phần tử giữ trạng thái sử dụng logic nghiệp vụ phải có các thuộc tính sau:

Thuộc tính Chi tiết
Tạo trạng thái giao diện người dùng Phần tử giữ trạng thái logic nghiệp vụ chịu trách nhiệm tạo trạng thái giao diện người dùng cho giao diện người dùng của chúng. Trạng thái giao diện người dùng này thường là kết quả của quá trình xử lý sự kiện người dùng và đọc dữ liệu từ miền và các lớp dữ liệu.
Được giữ lại thông qua quá trình tạo lại hoạt động Phần tử giữ trạng thái logic nghiệp vụ giữ lại quy trình xử lý trạng thái và trạng thái của chúng trong quá trình tạo lại Activity, giúp cung cấp trải nghiệm người dùng liền mạch. Trong trường hợp không thể giữ và tạo lại phần tử giữ trạng thái (thường là sau khi quá trình bị gián đoạn), thì phần tử giữ trạng thái phải có thể dễ dàng tạo lại trạng thái gần đây nhất để đảm bảo trải nghiệm nhất quán cho người dùng.
Sở hữu các trạng thái tồn tại lâu phần tử giữ trạng thái logic nghiệp vụ thường được dùng để quản lý trạng thái của các đích điều hướng. Do đó, chúng thường giữ nguyên trạng thái trên các thay đổi điều hướng cho đến khi bị xoá khỏi biểu đồ điều hướng.
Là duy nhất cho giao diện người dùng và không thể sử dụng lại Phần tử giữ trạng thái logic nghiệp vụ thường tạo trạng thái cho một chức năng nhất định của ứng dụng, chẳng hạn như TaskEditViewModel hoặc TaskListViewModel, nên chỉ áp dụng cho chức năng đó của ứng dụng. Phần tử giữ trạng thái đó có thể hỗ trợ các chức năng của ứng dụng này trên nhiều hệ số hình dạng. Ví dụ như các phiên bản dành cho thiết bị di động, TV và máy tính bảng của ứng dụng có thể sử dụng lại cùng một phần tử giữ trạng thái logic nghiệp vụ.

Ví dụ như hãy xem xét đích điều hướng của tác giả trong ứng dụng "Now in Android":

Ứng dụng Now in Android minh hoạ cách một đích điều hướng đại diện cho một chức năng chính của ứng dụng phải có phần tử giữ trạng thái logic nghiệp vụ riêng.
Hình 4: Ứng dụng Now in Android

Với vai trò là phần tử giữ trạng thái logic nghiệp vụ, AuthorViewModel sẽ tạo ra trạng thái giao diện người dùng trong trường hợp sau:

@HiltViewModel
class AuthorViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val authorsRepository: AuthorsRepository,
    newsRepository: NewsRepository
) : ViewModel() {

    val uiState: StateFlow<AuthorScreenUiState> = …

    // Business logic
    fun followAuthor(followed: Boolean) {
      …
    }
}

Lưu ý là AuthorViewModel có các thuộc tính đã nêu trước đây:

Thuộc tính Chi tiết
Tạo AuthorScreenUiState AuthorViewModel đọc dữ liệu từ AuthorsRepositoryNewsRepository, đồng thời sử dụng dữ liệu đó để tạo AuthorScreenUiState. Hoạt động này cũng áp dụng logic nghiệp vụ khi người dùng muốn theo dõi hoặc ngừng theo dõi Author bằng cách uỷ quyền cho AuthorsRepository.
Có quyền truy cập vào lớp dữ liệu Một thực thể của AuthorsRepositoryNewsRepository đã được truyền vào thực thể trong hàm khởi tạo, cho phép thực hiện logic nghiệp vụ của việc tuân theo Author.
Việc tạo lại Activity tiếp tục có hiệu lực Vì được triển khai bằng ViewModel, thuộc tính này sẽ được giữ lại qua việc tạo lại nhanh Activity. Trong trường hợp quá trình bị gián đoạn, đối tượng SavedStateHandle có thể được đọc để cung cấp lượng thông tin tối thiểu cần thiết nhằm khôi phục trạng thái giao diện người dùng từ lớp dữ liệu.
Sở hữu trạng thái tồn tại lâu ViewModel nằm trong phạm vi của biểu đồ điều hướng, do đó, trừ phi đích tác giả bị xoá khỏi biểu đồ điều hướng, trạng thái giao diện người dùng trong uiState StateFlow vẫn còn ở bộ nhớ. Việc sử dụng StateFlow cũng bổ sung lợi ích của việc áp dụng logic nghiệp vụ tạo ra trạng thái tải từng phần, vì trạng thái chỉ được tạo nếu có một trình thu thập trạng thái giao diện người dùng.
Là duy nhất cho giao diện người dùng AuthorViewModel chỉ áp dụng cho đích đến điều hướng tác giả và không thể sử dụng lại ở bất kỳ nơi nào khác. Nếu có bất kỳ logic nghiệp vụ nào được sử dụng lại trên các đích điều hướng, thì logic nghiệp vụ đó phải được gói gọn trong thành phần phạm vi lớp dữ liệu hoặc phạm vi lớp miền.

ViewModel với tư cách là phần tử giữ trạng thái logic nghiệp vụ

Lợi ích của ViewModel trong quá trình phát triển Android giúp lớp này phù hợp với việc cung cấp quyền truy cập vào logic nghiệp vụ và chuẩn bị dữ liệu ứng dụng để hiển thị trên màn hình. Các lợi ích này bao gồm:

  • Những hoạt động do ViewModel kích hoạt vẫn tồn tại sau những thay đổi về cấu hình.
  • Tích hợp với Điều hướng (Navigation):
    • Thành phần Điều hướng sẽ lưu các ViewModel vào bộ nhớ đệm trong khi màn hình đang ở ngăn xếp lui. Việc cung cấp dữ liệu được tải trước đó ngay lập tức khi bạn quay lại đích đến là rất quan trọng. Đây là một vấn đề khó thực hiện hơn với phần tử giữ trạng thái theo dõi vòng đời của màn hình thành phần kết hợp.
    • ViewModel cũng bị xoá khi điểm đến được kéo ra khỏi ngăn xếp lui, đảm bảo trạng thái của bạn được tự động dọn dẹp. Điều này khác với việc chuẩn bị cho trường hợp loại bỏ thành phần kết hợp có thể xảy ra vì nhiều lý do, chẳng hạn như khi chuyển sang màn hình mới, do sự thay đổi về cấu hình, v.v.
  • Tích hợp với các thư viện Jetpack khác như Hilt.

Logic giao diện người dùng và phần tử giữ trạng thái của nó

Logic giao diện người dùng là logic hoạt động trên dữ liệu mà chính giao diện người dùng cung cấp. Logic này có thể ở trạng thái của các thành phần trên giao diện người dùng hoặc trên các nguồn dữ liệu giao diện người dùng như API về quyền hoặc Resources. Các phần tử giữ trạng thái sử dụng logic giao diện người dùng thường có các thuộc tính sau:

  • Tạo trạng thái giao diện người dùng và quản lý trạng thái các thành phần giao diện người dùng.
  • Không tồn tại sau khi tạo lại Activity: Các phần tử giữ trạng thái được lưu trữ trong logic giao diện người dùng thường phụ thuộc vào các nguồn dữ liệu từ chính giao diện người dùng và tập trung giữ lại thông tin này qua các thay đổi về cấu hình thường xuyên hơn việc tránh gây ra sự cố rò rỉ bộ nhớ. Nếu phần tử giữ trạng thái cần dữ liệu để duy trì khi có các thay đổi về cấu hình, thì chúng cần được uỷ quyền cho một thành phần khác phù hợp hơn để tiếp tục tạo lại Activity. Ví dụ như trong Jetpack Compose, các trạng thái thành phần giao diện người dùng có thể kết hợp được tạo bằng các hàm remembered, thường uỷ quyền cho rememberSaveable để duy trì trạng thái trong quá trình tạo lại Activity. Ví dụ về các hàm này bao gồm rememberScaffoldState()rememberLazyListState().
  • Có thông tin tham chiếu đến các nguồn dữ liệu trong phạm vi giao diện người dùng: Các nguồn dữ liệu như API vòng đời và Tài nguyên có thể được tham chiếu và đọc một cách an toàn, vì phần tử giữ trạng thái logic của giao diện người dùng có cùng vòng đời với giao diện người dùng.
  • Có thể sử dụng lại trên nhiều giao diện người dùng: Có thể sử dụng lại các thực thể khác nhau của cùng một phần tử giữ trạng thái logic giao diện người dùng trong các phần của ứng dụng. Ví dụ như phần tử giữ trạng thái, để quản lý các sự kiện nhập của người dùng cho một nhóm chip có thể được dùng trên trang tìm kiếm các thẻ bộ lọc và cho trường "đến" của người nhận email.

phần tử giữ trạng thái logic của giao diện người dùng thường được triển khai bằng một lớp thuần tuý. Điều này là do giao diện người dùng tự chịu trách nhiệm về việc tạo phần tử giữ trạng thái logic giao diện người dùng và nó có cùng vòng đời với giao diện người dùng. Ví dụ như trong Jetpack Compose, phần tử giữ trạng thái là một phần của Thành phần cấu tạo và tuân theo vòng đời của Thành phần cấu tạo.

Điều này được minh hoạ ở ví dụ sau trong ứng dụng mẫu Now in Android:

Ứng dụng Now in Android sử dụng phần tử giữ trạng thái của lớp thuần tuý để quản lý logic giao diện người dùng
Hình 5: Ứng dụng mẫu Now in Android

Ứng dụng mẫu Now in Android cho thấy thanh ứng dụng ở dưới cùng hoặc thanh điều hướng của ứng dụng phụ thuộc vào kích thước màn hình của thiết bị. Màn hình nhỏ hơn sử dụng thanh ứng dụng ở dưới cùng, còn màn hình lớn hơn sử dụng dải điều hướng.

Vì logic để quyết định thành phần trên giao diện người dùng điều hướng thích hợp được sử dụng trong hàm kết hợp NiaApp không phụ thuộc vào logic nghiệp vụ, nên hàm này có thể được quản lý bằng một phần tử giữ trạng thái của lớp thuần tuý có tên là NiaAppState:

@Stable
class NiaAppState(
    val navController: NavHostController,
    val windowSizeClass: WindowSizeClass
) {

    // UI logic
    val shouldShowBottomBar: Boolean
        get() = windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact ||
            windowSizeClass.heightSizeClass == WindowHeightSizeClass.Compact

    // UI logic
    val shouldShowNavRail: Boolean
        get() = !shouldShowBottomBar

   // UI State
    val currentDestination: NavDestination?
        @Composable get() = navController
            .currentBackStackEntryAsState().value?.destination

    // UI logic
    fun navigate(destination: NiaNavigationDestination, route: String? = null) { /* ... */ }

     /* ... */
}

Trong ví dụ trên, bạn nên chú ý những thông tin chi tiết sau đây về NiaAppState:

  • Không tồn tại sau khi tạo lại Activity: NiaAppState được remembered trong Thành phần cấu tạo bằng cách tạo nó với một Hàm có khả năng kết hợp rememberNiaAppState tuân theo quy ước đặt tên của Compose. Sau khi tạo lại Activity, thực thể trước đó sẽ bị bỏ qua và một thực thể mới được tạo với tất cả các phần phụ thuộc được truyền vào, phù hợp với cấu hình mới của Activity được tạo lại. Những phần phụ thuộc này có thể mới hoặc được khôi phục từ cấu hình trước đó. Chẳng hạn như rememberNavController() được sử dụng trong hàm khởi tạo NiaAppState và uỷ quyền cho rememberSaveable để duy trì trạng thái trong quá trình tạo lại Activity.
  • Có thông tin tham chiếu đến các nguồn dữ liệu trong phạm vi giao diện người dùng: Tham chiếu đến navigationController, Resources và các loại tương tự khác trong phạm vi vòng đời có thể được giữ an toàn trong NiaAppState vì chúng có cùng phạm vi vòng đời.

Chọn giữa ViewModel và lớp thuần tuý cho phần tử giữ trạng thái

Từ các phần đã nói ở trên, việc lựa chọn giữa ViewModel và phần tử giữ trạng thái của lớp thuần tuý sẽ dẫn đến logic được áp dụng cho trạng thái giao diện người dùng và các nguồn dữ liệu mà logic hoạt động.

Tóm lại, sơ đồ dưới đây cho thấy vị trí của phần tử giữ trạng thái trong quy trình tạo trạng thái giao diện người dùng:

Dữ liệu di chuyển từ lớp tạo dữ liệu sang lớp giao diện người dùng
Hình 6: Phần tử giữ trạng thái trong quy trình tạo trạng thái giao diện người dùng. Các mũi tên minh hoạ cho luồng dữ liệu.

Cuối cùng, bạn nên tạo trạng thái giao diện người dùng bằng phần tử giữ trạng thái gần nhất với vị trí trạng thái đó được sử dụng.. Nói một cách đơn giản hơn, bạn nên giữ trạng thái ở mức thấp nhất có thể trong khi vẫn duy trì quyền sở hữu thích hợp. Nếu bạn cần quyền truy cập vào logic nghiệp vụ và cần trạng thái giao diện người dùng duy trì trong suốt thời gian màn hình điều hướng đến, kể cả trong quá trình tạo lại Activity, thì ViewModel là lựa chọn tuyệt vời cho việc triển khai phần tử giữ trạng thái logic nghiệp vụ. Đối với trạng thái giao diện người dùng và logic giao diện người dùng tồn tại ngắn hơn, bạn chỉ cần một lớp thuần tuý có vòng đời phụ thuộc hoàn toàn vào giao diện người dùng là đủ.

Phần tử giữ trạng thái có dạng phức hợp

Phần tử giữ trạng thái có thể phụ thuộc vào các phần tử giữ trạng thái khác, miễn là các phần phụ thuộc có thời gian tồn tại bằng hoặc ngắn hơn. Ví dụ:

  • phần tử giữ trạng thái logic của giao diện người dùng có thể phụ thuộc vào một phần tử giữ trạng thái logic của giao diện người dùng khác.
  • phần tử giữ trạng thái cấp màn hình có thể phụ thuộc vào phần tử giữ trạng thái logic của giao diện người dùng.

Đoạn mã sau đây cho biết cách DrawerState của Compose phụ thuộc vào một phần tử giữ trạng thái nội bộ khác là SwipeableState, cũng như cách phần tử giữ trạng thái logic của giao diện người dùng của ứng dụng có thể phụ thuộc vào DrawerState:

@Stable
class DrawerState(/* ... */) {
  internal val swipeableState = SwipeableState(/* ... */)
  // ...
}

@Stable
class MyAppState(
  private val drawerState: DrawerState,
  private val navController: NavHostController
) { /* ... */ }

@Composable
fun rememberMyAppState(
  drawerState: DrawerState = rememberDrawerState(DrawerValue.Closed),
  navController: NavHostController = rememberNavController()
): MyAppState = remember(drawerState, navController) {
  MyAppState(drawerState, navController)
}

Ví dụ về phần phụ thuộc tồn tại lâu hơn phần tử giữ trạng thái: phần tử giữ trạng thái logic của giao diện người dùng phụ thuộc vào phần tử giữ trạng thái cấp màn hình. Điều đó sẽ làm giảm khả năng sử dụng lại của phần tử giữ trạng thái có thời gian hoạt động ngắn hơn, cho phép nó truy cập vào nhiều logic và trạng thái hơn so với mức cần thiết.

Nếu phần tử giữ trạng thái tồn tại ngắn hơn cần những thông tin nhất định từ phần tử giữ trạng thái có phạm vi cao hơn, hãy chỉ truyền thông tin cần thiết dưới dạng tham số thay vì truyền thực thể của phần tử giữ trạng thái. Ví dụ: trong đoạn mã sau đây, lớp phần tử giữ trạng thái của giao diện người dùng chỉ nhận được những gì cần thiết dưới dạng tham số từ ViewModel, thay vì truyền toàn bộ thực thể ViewModel dưới dạng phần phụ thuộc.

class MyScreenViewModel(/* ... */) {
  val uiState: StateFlow<MyScreenUiState> = /* ... */
  fun doSomething() { /* ... */ }
  fun doAnotherThing() { /* ... */ }
  // ...
}

@Stable
class MyScreenState(
  // DO NOT pass a ViewModel instance to a plain state holder class
  // private val viewModel: MyScreenViewModel,

  // Instead, pass only what it needs as a dependency
  private val someState: StateFlow<SomeState>,
  private val doSomething: () -> Unit,

  // Other UI-scoped types
  private val scaffoldState: ScaffoldState
) {
  /* ... */
}

@Composable
fun rememberMyScreenState(
  someState: StateFlow<SomeState>,
  doSomething: () -> Unit,
  scaffoldState: ScaffoldState = rememberScaffoldState()
): MyScreenState = remember(someState, doSomething, scaffoldState) {
  MyScreenState(someState, doSomething, scaffoldState)
}

@Composable
fun MyScreen(
  modifier: Modifier = Modifier,
  viewModel: MyScreenViewModel = viewModel(),
  state: MyScreenState = rememberMyScreenState(
    someState = viewModel.uiState.map { it.toSomeState() },
    doSomething = viewModel::doSomething
  ),
  // ...
) {
  /* ... */
}

Sơ đồ dưới đây thể hiện các phần phụ thuộc giữa giao diện người dùng và nhiều phần tử giữ trạng thái của đoạn mã trước đó:

Giao diện người dùng phụ thuộc vào cả phần tử giữ trạng thái logic của giao diện người dùng và phần tử giữ trạng thái cấp màn hình
Hình 7: Giao diện người dùng phụ thuộc vào nhiều phần tử giữ trạng thái. Các mũi tên minh hoạ cho những phần phụ thuộc.

Mẫu

Các mẫu sau đây của Google minh hoạ việc sử dụng phần tử giữ trạng thái 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ế: