Trạng thái và Jetpack Compose

Trạng thái (state) trong ứng dụng là giá trị bất kỳ có thể thay đổi theo thời gian. Đây là định nghĩa rất rộng và bao gồm mọi thứ từ cơ sở dữ liệu Room cho đến một biến trên một lớp (class).

Tất cả ứng dụng Android đều cho người dùng thấy trạng thái. Sau đây là một số ví dụ về trạng thái trong ứng dụng Android:

  • Một thanh thông báo nhanh cho biết thời điểm không thể thiết lập kết nối mạng.
  • Một bài đăng trên blog và các bình luận liên quan.
  • Hoạt ảnh gợn sóng trên các nút phát khi người dùng nhấp vào.
  • Hình dán mà người dùng có thể vẽ lên hình ảnh.

Jetpack Compose giúp bạn hiểu rõ địa điểm và cách lưu trữ cũng như sử dụng trạng thái trong một ứng dụng Android. Tài liệu hướng dẫn này tập trung vào hoạt động kết nối giữa các trạng thái và thành phần kết hợp, đồng thời tập trung vào các API mà Jetpack Compose cung cấp để xử lý trạng thái dễ dàng hơn.

Trạng thái và thành phần

Compose mang tính khai báo và vì vậy, cách duy nhất để cập nhật Compose là gọi cùng một thành phần kết hợp (composable) với đối số mới. Các đối số này là đại diện cho trạng thái giao diện người dùng. Mỗi khi một trạng thái được cập nhật, một lượt tái cấu trúc (recomposition) diễn ra. Do đó, những thành phần như TextField không tự động cập nhật như trong thành phần hiển thị dựa trên XML bắt buộc. Một thành phần kết hợp phải được thông báo rõ ràng về trạng thái mới để cập nhật tương ứng.

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       Text(
           text = "Hello!",
           modifier = Modifier.padding(bottom = 8.dp),
           style = MaterialTheme.typography.h5
       )
       OutlinedTextField(
           value = "",
           onValueChange = { },
           label = { Text("Name") }
       )
   }
}

Nếu thực hiện việc này, bạn sẽ thấy rằng không có điều gì xảy ra. Nguyên nhân là do TextField không tự cập nhật, mà sẽ cập nhật khi tham số value của nó thay đổi. Lý do nằm ở cách hoạt động của tính năng cấu trúc (composition) và tái cấu trúc (recomposition) trong Compose.

Để tìm hiểu thêm về Cấu trúc ban đầu và Tái cấu trúc, hãy xem Cách suy nghĩ trong Compose.

Trạng thái trong thành phần kết hợp

Các hàm kết hợp có thể sử dụng API remember để lưu trữ đối tượng trong bộ nhớ. Một giá trị do remember tính toán được lưu trữ trong Bố cục của quá trình cấu trúc ban đầu, và giá trị đã lưu trữ được trả về trong quá trình tái cấu trúc. Bạn có thể dùng remember để lưu trữ cả đối tượng có thể thay đổi và không thể thay đổi.

mutableStateOf tạo ra một đối tượng phát ra dữ liệu MutableState<T>, đây là một loại đối tượng phát ra dữ liệu được tích hợp với thời gian chạy của ứng dụng soạn thư.

interface MutableState<T> : State<T> {
    override var value: T
}

Mọi thay đổi đối với value sẽ lên lịch tái cấu trúc mọi hàm kết hợp có thể đọc value. Trong trường hợp ExpandingCard, bất cứ khi nào expanded thay đổi, hệ thống sẽ tái cấu trúc ExpandingCard.

Có ba cách để khai báo đối tượng MutableState trong một thành phần kết hợp:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

Các thông tin khai báo này là tương đương và được cung cấp dưới dạng cú pháp dễ hiểu theo mục đích sử dụng của trạng thái. Bạn nên chọn định dạng tạo ra mã dễ đọc nhất trong thành phần kết hợp mà bạn đang viết.

Cú pháp uỷ quyền (delegate syntax) by yêu cầu các lần nhập sau:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Bạn có thể sử dụng giá trị đã ghi nhớ làm tham số cho các thành phần kết hợp khác hoặc thậm chí là logic trong các câu lệnh để thay đổi thành phần kết hợp nào được hiển thị. Ví dụ: nếu bạn không muốn hiện lời chào nếu phần tên trống, hãy sử dụng trạng thái trong câu lệnh if:

@Composable
fun HelloContent() {
   Column(modifier = Modifier.padding(16.dp)) {
       var name by remember { mutableStateOf("") }
       if (name.isNotEmpty()) {
           Text(
               text = "Hello, $name!",
               modifier = Modifier.padding(bottom = 8.dp),
               style = MaterialTheme.typography.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

Mặc dù remember giúp bạn giữ lại trạng thái trên các lần tái cấu trúc, trạng thái này sẽ không được giữ lại khi bạn thay đổi cấu hình. Để làm được điều này, bạn phải sử dụng rememberSaveable. rememberSaveable tự động lưu mọi giá trị có thể lưu trong Bundle. Đối với các giá trị khác, bạn có thể truyền vào một đối tượng lưu tuỳ chỉnh.

Các loại trạng thái được hỗ trợ khác

Jetpack Compose không đòi hỏi bạn phải dùng MutableState<T> để giữ trạng thái. Jetpack Compose hỗ trợ các loại đối tượng phát ra dữ liệu khác. Trước khi đọc một loại đối tượng phát ra dữ liệu khác trong Jetpack Compose, bạn phải chuyển đổi dữ liệu thành State<T> để Jetpack Compose có thể tự động tái cấu trúc khi trạng thái thay đổi.

Compose cung cấp các hàm để tạo State<T> từ những loại đối tượng phát ra dữ liệu phổ biến được dùng trong các ứng dụng Android:

Bạn có thể tạo hàm tiện ích cho Jetpack Compose để đọc các loại đối tượng phát ra dữ liệu khác nếu ứng dụng của bạn sử dụng loại đối tượng phát ra dữ liệu tuỳ chỉnh. Hãy xem ví dụ về cách triển khai các thuộc tính tích hợp sẵn. Mọi đối tượng cho phép Jetpack Compose đăng ký mọi thay đổi đều có thể được chuyển đổi thành State<T> và được đọc bằng một thành phần kết hợp.

Trạng thái so với không có trạng thái

Một thành phần kết hợp sử dụng remember để lưu trữ một đối tượng sẽ tạo trạng thái nội bộ, giúp thành phần kết hợp có trạng thái (stateful). HelloContent là một ví dụ về thành phần kết hợp có trạng thái vì nó chứa và sửa đổi trạng thái name nội tại. Điều này có thể hữu ích trong trường hợp trình gọi không cần kiểm soát trạng thái và có thể sử dụng mà không phải tự quản lý trạng thái. Tuy nhiên, các thành phần kết hợp với trạng thái nội bộ có xu hướng ít có khả năng tái sử dụng và khó thử nghiệm hơn.

Thành phần kết hợp không có trạng thái (stateless) là thành phần kết hợp không thuộc một trạng thái nào. Một cách dễ dàng để đạt được trạng thái không có trạng thái là sử dụng tính năng chuyển trạng thái lên trên (state hoisting).

Khi phát triển các thành phần kết hợp có thể sử dụng lại, bạn thường muốn hiện cả phiên bản trạng thái và phiên bản không có trạng thái của cùng một thành phần kết hợp. Phiên bản có trạng thái sẽ hữu ích cho trình gọi không quan tâm đến trạng thái, và phiên bản không trạng thái cần thiết cho trình gọi cần kiểm soát hoặc di chuyển trạng thái lên trên.

Chuyển trạng thái lên trên

Tính năng chuyển trạng thái lên trên (state hoisting) trong Compose là một dạng chuyển đổi trạng thái cho trình gọi của một thành phần kết hợp khiến nó trở thành không trạng thái. Mô hình chung cho việc di chuyển trạng thái lên trên trong Jetpack Compose là thay thế biến trạng thái bằng hai tham số:

  • value: T: giá trị hiện tại để hiển thị
  • onValueChange: (T) -> Unit: một sự kiện yêu cầu thay đổi giá trị này, trong đó T là giá trị mới được đề xuất

Tuy nhiên, bạn không bị giới hạn ở onValueChange. Nếu các sự kiện cụ thể hơn phù hợp với thành phần kết hợp, bạn nên xác định sự kiện bằng cách sử dụng lambda như ExpandingCard với onExpandonCollapse.

Trạng thái được di chuyển lên trên theo cách này có một số thuộc tính quan trọng:

  • Một nguồn đáng tin cậy (single source of truth): Bằng cách di chuyển trạng thái thay vì sao chép, chúng tôi đảm bảo rằng chỉ có một nguồn thông tin duy nhất. Điều này giúp tránh các lỗi.
  • Tính gói gọn (encapsulated): Chỉ các thành phần kết hợp có trạng thái mới có thể sửa đổi trạng thái của chúng. Nó có tính nội bộ hoàn toàn.
  • Có thể chia sẻ (shareable): Bạn có thể chia sẻ trạng thái được di chuyển lên trên với nhiều thành phần kết hợp. Giả sử chúng ta muốn name trong một thành phần kết hợp khác, việc di chuyển lên trên sẽ cho phép chúng ta làm việc đó.
  • Có thể chắn (interceptable): trình gọi đến các thành phần kết hợp không trạng thái có thể quyết định bỏ qua hoặc sửa đổi các sự kiện trước khi thay đổi trạng thái.
  • Được tách riêng (decoupled): trạng thái của ExpandingCard không có trạng thái có thể được lưu trữ ở bất cứ đâu. Ví dụ: bạn hiện có thể di chuyển name sang ViewModel.

Trong trường hợp ví dụ, bạn trích xuất nameonValueChange ra HelloContent và di chuyển chúng lên trên đến một thành phần kết hợp HelloScreen bằng lệnh gọi HelloContent.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

Bằng cách nâng trạng thái ra khỏi HelloContent, sẽ dễ dàng cho việc lý giải thành phần kết hợp, tái sử dụng trong nhiều tình huống và thử nghiệm. HelloContent được tách riêng khỏi cách lưu trữ trạng thái của tệp. Việc tách riêng có nghĩa là nếu sửa đổi hoặc thay thế HelloScreen, bạn không phải thay đổi cách thức triển khai HelloContent.

Mô hình mà trạng thái giảm và các sự kiện tăng lên được gọi là luồng dữ liệu một chiều (unidirectional). Trong trường hợp này, trạng thái giảm từ HelloScreen xuống HelloContent và các sự kiện tăng từ HelloContent lên HelloScreen. Bằng cách sử dụng luồng dữ liệu một chiều, bạn có thể tách riêng các thành phần kết hợp hiển thị trạng thái trong giao diện người dùng từ các phần của ứng dụng có tính năng lưu trữ và thay đổi trạng thái.

Khôi phục trạng thái trong Compose

Sử dụng rememberSaveable để khôi phục trạng thái trên giao diện người dùng sau khi tái tạo một hoạt động hoặc quy trình. rememberSaveable giữ lại trạng thái trên các quá trình tái cấu trúc Ngoài ra, rememberSaveable còn giữ nguyên trạng thái trên quá trình tái tạo một hoạt động hoặc quy trình.

Các cách lưu trữ trạng thái

Tất cả loại dữ liệu được thêm vào Bundle sẽ được lưu tự động. Nếu muốn lưu nội dung nào đó mà không thể thêm vào Bundle, bạn có một số tuỳ chọn.

Parcelize

Cách đơn giản nhất là thêm chú thích @Parcelize vào đối tượng. Đối tượng trở nên đóng gói được và nhóm được. Ví dụ: mã này tạo một loại dữ liệu City có thể đóng gói và lưu dữ liệu vào trạng thái.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

Nếu vì lý do nào đó @Parcelize không phù hợp, bạn có thể sử dụng mapSaver để xác định quy tắc của riêng mình để chuyển đổi một đối tượng thành một tập hợp giá trị mà hệ thống có thể lưu vào Bundle.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

Để tránh phải xác định các khoá cho bản đồ, bạn cũng có thể dùng listSaver và sử dụng các chỉ mục làm khoá:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Quản lý trạng thái trong Compose

Có thể quản lý quá trình di chuyển trạng thái lên trên đơn giản trong chính các hàm có khả năng kết hợp. Tuy nhiên, nếu số lượng trạng thái cần theo dõi tăng lên, hoặc logic để thực hiện trong các hàm có khả năng kết hợp phát sinh, bạn nên uỷ quyền trách nhiệm về logic và trạng thái cho các lớp khác: chủ thể trạng thái (state holder).

Phần này trình bày cách quản lý trạng thái theo nhiều cách trong Compose. Tuỳ thuộc vào mức độ phức tạp thành phần kết hợp, có nhiều phương án để bạn xem xét:

  • Thành phần kết hợp (Composable) để quản lý trạng thái thành phần giao diện người dùng đơn giản.
  • Chủ thể trạng thái (State holder) để quản lý trạng thái thành phần giao diện người dùng phức tạp. Chúng sở hữu trạng thái của các thành phần giao diện người dùng và logic giao diện người dùng.
  • ViewModel cấu trú (Architecture Components ViewModels) là một loại chủ thể trạng thái đặc biệt chịu trách nhiệm cung cấp quyền truy cập vào logic nghiệp vụ và màn hình hoặc trạng thái giao diện người dùng.

Các chủ thể trạng thái có nhiều kích thước, tuỳ thuộc vào phạm vi của các thành phần giao diện người dùng tương ứng mà người dùng quản lý, từ một tiện ích đơn lẻ như thanh ứng dụng ở dưới cùng cho đến toàn bộ màn hình. Chủ thể trạng thái ở dạng phức hợp; nghĩa là một chủ thể trạng thái có thể được tích hợp vào một chủ thể trạng thái khác, đặc biệt là khi tổng hợp các trạng thái.

Sơ đồ dưới đây tóm tắt mối quan hệ giữa các thực thể có liên quan đến tính năng quản lý trạng thái của Compose. Phần còn lại trình bày từng thực thể một cách chi tiết:

  • Một thành phần kết hợp có thể phụ thuộc vào 0 hoặc nhiều chủ thể trạng thái (có thể là đối tượng thuần tuý (plain), ViewModel hoặc cả hai) tuỳ thuộc vào độ phức tạp của cấu trúc đó.
  • Chủ thể trạng thái thuần tuý có thể phụ thuộc vào ViewModel nếu cần quyền truy cập vào logic nghiệp vụ hoặc trạng thái màn hình.
  • ViewModel tuỳ thuộc vào nghiệp vụ hoặc các tầng dữ liệu.

Sơ đồ cho thấy các phần phụ thuộc trong tính năng quản lý trạng thái, như được mô tả trong danh sách trước đó.

Tóm tắt các phần phụ thuộc (không bắt buộc) cho từng thực thể liên quan đến tính năng quản lý trạng thái của Compose.

Các loại trạng thái và logic

Trong một ứng dụng Android, bạn cần cân nhắc nhiều loại trạng thái sau:

  • Trạng thái thành phần giao diện người dùng là trạng thái di chuyển lên trên của các thành phần giao diện người dùng. Ví dụ: ScaffoldState sẽ xử lý trạng thái của thành phần kết hợp Scaffold.

  • Trạng thái màn hình hoặc giao diện người dùngnội dung cần hiện trên màn hình. Ví dụ: một lớp CartUiState có thể chứa các mục Giỏ hàng, các tin nhắn cần cho người dùng thấy hoặc cờ đang tải. Trạng thái này thường được kết nối với các tầng khác trong hệ phân cấp vì nó chứa dữ liệu ứng dụng.

Ngoài ra, có nhiều loại logic:

  • Logic của hành vi giao diện người dùng hoặc logic giao diện người dùng liên quan đến cách hiện các thay đổi về trạng thái trên màn hình. Ví dụ: logic điều hướng quyết định màn hình nào sẽ hiện tiếp theo, hoặc logic giao diện người dùng quyết định cách hiện tin nhắn người dùng trên màn hình (có thể là sử dụng thanh thông báo nhanh hoặc thông báo ngắn). Logic hành vi giao diện người dùng phải luôn tồn tại trong Composition.

  • Logic nghiệp vụnhững việc cần làm với các thay đổi trạng thái Ví dụ: thanh toán hoặc lưu trữ các tuỳ chọn của người dùng. Logic này thường được đặt trong các lớp nghiệp vụ hoặc dữ liệu, không bao giờ được đặt trong lớp giao diện người dùng.

Thành phần kết hợp là nguồn đáng tin

Việc sử dụng logic và các thành phần trên giao diện người dùng ở trạng thái tương ứng trong các thành phần kết hợp tương ứng là một phương pháp hữu ích nếu trạng thái và logic đó là đơn giản. Ví dụ: dưới đây là thành phần kết hợp MyApp xử lý ScaffoldStateCoroutineScope:

@Composable
fun MyApp() {
    MyTheme {
        val scaffoldState = rememberScaffoldState()
        val coroutineScope = rememberCoroutineScope()

        Scaffold(scaffoldState = scaffoldState) {
            MyContent(
                showSnackbar = { message ->
                    coroutineScope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(message)
                    }
                }
            )
        }
    }
}

ScaffoldState chứa các thuộc tính có thể thay đổi, tất cả lượt tương tác với nó đều xảy ra trong thành phần kết hợp MyApp Nếu không, nếu chúng ta truyền nó đến các thành phần kết hợp khác, các trình thu thập đó có thể thay đổi trạng thái của các chế độ cài đặt đó, trạng thái này không tuân thủ nguyên tắc một nguồn đáng tin và khiến việc theo dõi lỗi trở nên khó khăn hơn.

Chủ thể trạng thái là nguồn đáng tin

Khi một thành phần kết hợp chứa logic giao diện người dùng phức tạp có liên quan đến trạng thái của nhiều thành phần giao diện người dùng, giao thức đó sẽ uỷ quyền trách nhiệm đó cho các chủ thể trạng thái. Điều này giúp logic này dễ kiểm tra hơn khi tách biệt và giảm tính phức tạp của thành phần kết hợp. Phương pháp này ưa chuộng nguyên tắc tách biệt vấn đề: thành phần kết hợp chịu trách nhiệm chuyển phát (emit) các thành phần giao diện người dùng còn chủ thể trạng thái chứa logic giao diện người dùng và trạng thái của thành phần giao diện người dùng.

Chủ thể trạng thái là các lớp thuần tuý được tạo ra và ghi nhớ trong Cấu trúc. Vì chúng tuân theo vòng đời của thành phần kết hợp, chúng có thể sử dụng các phần phụ thuộc của Compose

Nếu thành phần kết hợp MyApp từ phần thành phần kết hợp dưới dạng nguồn đáng tin thực hiện đúng trách nhiệm, chúng ta có thể tạo một chủ thể trạng thái MyAppState để quản lý sự phức tạp của mục đó:

// Plain class that manages App's UI logic and UI elements' state
class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources,
    /* ... */
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
    /* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}

MyAppState sử dụng các phần phụ thuộc, bạn cần cung cấp một phương thức để ghi nhớ bản sao của MyAppState trong Cấu trúc. Trong trường hợp này, hàm rememberMyAppState.

Hiện tại, MyApp tập trung vào việc chuyển phát thành phần giao diện người dùng và uỷ quyền tất cả logic giao diện người dùng và trạng thái thành phần giao diện người dùng vào MyAppState:

@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()
        Scaffold(
            scaffoldState = myAppState.scaffoldState,
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, "initial") { /* ... */ }
        }
    }
}

Như bạn có thể thấy, việc tăng cường trách nhiệm của thành phần kết hợp sẽ tăng nhu cầu của chủ thể trạng thái. Bạn có thể chịu trách nhiệm về logic giao diện người dùng hoặc chỉ cần theo dõi số lượng trạng thái.

ViewModel là nguồn đáng tin

Nếu các lớp của chủ thể trạng thái thuần tuý chịu trách nhiệm về trạng thái của giao diện người dùng và thành phần trên giao diện người dùng, ViewModel là một loại đặc biệt của chủ thể trạng thái chịu trách nhiệm:

  • cung cấp quyền truy cập vào logic nghiệp vụ của ứng dụng thường được đặt trong các lớp khác của hệ phân cấp, chẳng hạn như lớp nghiệp vụ và dữ liệu, và
  • chuẩn bị dữ liệu ứng dụng để trình bày trong một màn hình cụ thể, màn hình này sẽ trở thành màn hình hoặc trạng thái giao diện người dùng.

ViewModel có thời gian tồn tại lâu hơn so với Composition vì các thành phần này vẫn tồn tại sau khi thay đổi cấu hình. Chúng có thể tuân theo vòng đời của máy chủ lưu trữ nội dung Compose (tức là các hoạt động hoặc mảnh) hoặc vòng đời của một đích đến hoặc Biểu đồ điều hướng nếu bạn đang sử dụng Thư viện điều hướng. Vì tồn tại lâu hơn, ViewModel không nên giữ lại các tệp tham chiếu dài hạn để giới hạn về thời gian tồn tại của Composition. Việc này có thể gây ra tình trạng rò rỉ bộ nhớ.

Chúng tôi đề xuất thành phần kết hợp cấp màn hình nên sử dụng với các thực thể ViewModel để cấp quyền truy cập vào logic kinh doanh và là nguồn thông tin cho trạng thái giao diện người dùng. Bạn không nên chuyển các thực thể ViewModel xuống các thành phần kết hợp khác. Hãy kiểm tra phần ViewModel và chủ thể trạng thái để biết vì sao ViewModel có thể được dùng cho trường hợp này.

Sau đây là ví dụ về ViewModel được sử dụng trong thành phần kết hợp cấp màn hình:

data class ExampleUiState(
    val dataToDisplayOnScreen: List<Example> = emptyList(),
    val userMessages: List<Message> = emptyList(),
    val loading: Boolean = false
)

class ExampleViewModel(
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf(ExampleUiState())
        private set

    // Business logic
    fun somethingRelatedToBusinessLogic() { /* ... */ }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    /* ... */

    ExampleReusableComponent(
        someData = uiState.dataToDisplayOnScreen,
        onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
    )
}

@Composable
fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
    /* ... */
    Button(onClick = onDoSomething) {
        Text("Do something")
    }
}

ViewModel và chủ thể trạng thái

Lợi ích của ViewModel trong quá trình phát triển Android là giúp các ứng dụng 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ụ thể, có các lợi ích sau:

  • Những hoạt động kích hoạt bởi ViewModel 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):
    • Thao tác 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 là rất quan trọng. Đây là một vấn đề khó thực hiện hơn với chủ thể 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 bật 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 trình nghe loại bỏ thành phần kết hợp (composable disposal) 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 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.

Vì chủ thể trạng thái ở dạng phức hợp và ViewModel và chủ thể trạng thái thuần tuý có trách nhiệm khác nhau, một thành phần kết hợp cấp màn hình có thể có cả một ViewModel cung cấp quyền truy cập vào logic nghiệp vụ một chủ thể trạng thái quản lý trạng thái của logic và các thành phần trên giao diện người dùng. Vì ViewModel có vòng đời dài hơn so với chủ thể trạng thái, chủ thể trạng thái có thể coi ViewModel là phần phụ thuộc nếu cần.

Mã sau đây cho thấy một ViewModel và chủ thể trạng thái thuần tuý cùng hoạt động trên ExampleScreen:

class ExampleState(
    val lazyListState: LazyListState,
    private val resources: Resources,
    private val expandedItems: List<Item> = emptyList()
) {
    fun isExpandedItem(item: Item): Boolean = TODO()
    /* ... */
}

@Composable
fun rememberExampleState(/* ... */): ExampleState { TODO() }

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    val exampleState = rememberExampleState()

    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item ->
            if (exampleState.isExpandedItem(item)) {
                /* ... */
            }
            /* ... */
        }
    }
}

Tìm hiểu thêm

Để tìm hiểu thêm về trạng thái và Jetpack Compose, hãy tham khảo thêm các tài nguyên sau đây:

Lớp học lập trình

Video