Trạng thái trong Jetpack Compose

1. Trước khi bắt đầu

Lớp học lập trình này giải thích các khái niệm chính liên quan đến việc sử dụng Trạng thái trong Jetpack Compose. Qua đó, giúp bạn nắm được cách trạng thái của ứng dụng xác định nội dung xuất hiện trên giao diện người dùng, cách Compose làm việc với API để cập nhật giao diện người dùng khi trạng thái thay đổi, cách tối ưu hoá cấu trúc của hàm có khả năng kết hợp cũng như cách sử dụng ViewModel trong thế giới Compose.

Điều kiện tiên quyết

  • Có kiến thức về cú pháp Kotlin.
  • Hiểu biết cơ bản về Compose (bạn có thể bắt đầu với hướng dẫn về Jetpack Compose).
  • Hiểu biết cơ bản về ViewModel của Thành phần kiến trúc.

Kiến thức bạn sẽ học được

  • Tìm hiểu cách hoạt động của trạng thái và sự kiện trong giao diện người dùng Jetpack Compose.
  • Cách Compose sử dụng trạng thái để xác định các phần tử hiển thị trên màn hình.
  • Chuyển trạng thái lên trên.
  • Cách các hàm có khả năng kết hợp có trạng thái và không có trạng thái hoạt động.
  • Cách Compose tự động theo dõi trạng thái bằng API State<T>.
  • Cách bộ nhớ và trạng thái nội bộ hoạt động trong một hàm có khả năng kết hợp: sử dụng API rememberrememberSaveable.
  • Cách xử lý danh sách và trạng thái: sử dụng API mutableStateListOftoMutableStateList.
  • Cách sử dụng ViewModel bằng Compose.

Bạn cần có

Đề xuất/không bắt buộc

Sản phẩm bạn sẽ tạo ra

Bạn sẽ triển khai một ứng dụng Wellness đơn giản:

775940a48311302b.png

Ứng dụng sẽ có hai chức năng chính:

  • Một đồng hồ nước để theo dõi lượng nước bạn uống.
  • Danh sách các nhiệm vụ để chăm sóc sức khoẻ mỗi ngày.

Để được hỗ trợ thêm khi tham gia lớp học lập trình này, hãy xem các nội dung tập lập trình dưới đây:

2. Bắt đầu thiết lập

Bắt đầu một dự án Compose mới

  1. Để bắt đầu một dự án Compose mới, vui lòng mở Android Studio.
  2. Nếu bạn đang ở cửa sổ Welcome to Android Studio (Chào mừng bạn đến với Android Studio), vui lòng nhấp vào nút Start a new Android Studio (Bắt đầu dự án Android Studio mới). Nếu bạn đã mở một dự án Android Studio, hãy chọn File > New > New Project (Tệp > Mới > Dự án mới) trong thanh trình đơn.
  3. Đối với dự án mới, hãy chọn Empty Activity (Hoạt động trống) trong số các mẫu có sẵn.

Dự án mới

  1. Nhấp vào Next (Tiếp theo) và định cấu hình dự án, gọi dự án là "BasicStateCodelab".

Hãy nhớ chọn minimumSdkVersion tối thiểu là API cấp 21, đây là API tối thiểu mà Compose hỗ trợ.

Khi bạn chọn mẫu Empty Compose Activity (Hoạt động trống trong Compose), Android Studio sẽ thiết lập các tài nguyên sau cho bạn trong dự án:

  • Một lớp MainActivity được định cấu hình bằng hàm có khả năng kết hợp hiển thị một số văn bản trên màn hình.
  • Tệp AndroidManifest.xml xác định quyền, thành phần và tài nguyên tuỳ chỉnh của ứng dụng.
  • Các tệp build.gradle.ktsapp/build.gradle.kts chứa các lựa chọn và phần phụ thuộc cần thiết cho Compose.

Giải pháp cho lớp học lập trình

Bạn có thể lấy mã giải pháp cho BasicStateCodelab từ GitHub:

$ git clone https://github.com/android/codelab-android-compose

Ngoài ra, bạn có thể tải kho lưu trữ xuống dưới dạng tệp Zip.

Bạn sẽ tìm thấy đoạn mã giải pháp trong dự án BasicStateCodelab. Bạn nên làm theo hướng dẫn từng bước trong lớp học lập trình theo tốc độ riêng và xem phần giải pháp nếu cần trợ giúp. Xuyên suốt lớp học lập trình, bạn sẽ thấy các đoạn mã bạn cần thêm vào dự án.

3. Trạng thái trong Compose

"Trạng thái" của ứ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. Dưới đây là một số ví dụ về trạng thái trong ứng dụng Android:

  • Tin nhắn nhận được gần đây nhất trong ứng dụng trò chuyện.
  • Ảnh hồ sơ của người dùng.
  • Vị trí cuộn trong danh sách các mục.

Hãy bắt đầu viết ứng dụng Sức khoẻ của bạn.

Để đơn giản hoá, trong lớp học lập trình này:

  • Bạn có thể thêm tất cả tệp Kotlin trong gói com.codelabs.basicstatecodelab gốc của mô-đun app. Tuy nhiên, đối với ứng dụng chính thức, tệp phải được cấu trúc hợp lý trong các gói con.
  • Bạn sẽ mã hoá cứng tất cả các chuỗi cùng dòng trong đoạn mã. Trong một ứng dụng thực tế, bạn nên thêm các tài nguyên này dưới dạng tài nguyên chuỗi trong tệp strings.xml, đồng thời tham chiếu bằng API stringResource của Compose.

Chức năng đầu tiên bạn cần xây dựng là bộ đếm nước để đếm số lượng ly nước bạn tiêu thụ trong ngày.

Tạo một hàm có khả năng kết hợp có tên là WaterCounter chứa thành phần kết hợp Text cho thấy số lượng ly nước. Số lượng ly nước sẽ được lưu trữ trong một giá trị có tên là count. Hiện tại, bạn có thể mã hoá cứng các ly nước này.

Tạo một tệp mới WaterCounter.kt bằng hàm có khả năng kết hợp WaterCounter như bên dưới:

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   val count = 0
   Text(
       text = "You've had $count glasses.",
       modifier = modifier.padding(16.dp)
   )
}

Hãy tạo một hàm có khả năng kết hợp đại diện cho toàn bộ màn hình, gồm hai phần là bộ đếm nước và danh sách nhiệm vụ chăm sóc sức khoẻ. Giờ thì chúng ta chỉ cần thêm bộ đếm.

  1. Tạo một tệp WellnessScreen.kt đại diện cho màn hình chính, sau đó gọi hàm WaterCounter:
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   WaterCounter(modifier)
}
  1. Mở MainActivity.kt. Xoá các thành phần kết hợp GreetingDefaultPreview. Gọi thành phần kết hợp WellnessScreen mới tạo bên trong khối setContent của Hoạt động, như bên dưới:
class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           BasicStateCodelabTheme {
               // A surface container using the 'background' color from the theme
               Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colorScheme.background
               ) {
                   WellnessScreen()
               }
           }
       }
   }
}
  1. Nếu chạy ứng dụng ngay thì bạn sẽ thấy màn hình bộ đếm nước cơ bản có số lượng ly nước được mã hoá cứng.

7ed1e6fbd94bff04.jpeg

Trạng thái của hàm có khả năng kết hợp WaterCounter là biến count. Tuy nhiên, việc có trạng thái tĩnh không hữu ích lắm vì không thể sửa đổi được. Để khắc phục sự cố này, bạn sẽ thêm tham số Button để tăng số lượng và theo dõi lượng nước bạn uống trong ngày.

Bất kỳ hành động nào dẫn đến việc sửa đổi trạng thái đều được gọi là "sự kiện", chúng ta sẽ tìm hiểu thêm về các sự kiện này trong phần tiếp theo.

4. Sự kiện trong Compose

Chúng ta đã nói về trạng thái dưới dạng mọi giá trị thay đổi theo thời gian (ví dụ: tin nhắn cuối cùng nhận được trong ứng dụng nhắn tin). Nhưng điều gì giúp cập nhật trạng thái? Trong các ứng dụng Android, trạng thái được cập nhật để phản hồi các sự kiện.

Sự kiện là các thông tin đầu vào được tạo từ bên ngoài hoặc bên trong một ứng dụng, chẳng hạn như:

  • Người dùng tương tác với giao diện người dùng bằng cách nhấn một nút chẳng hạn.
  • Các yếu tố khác, chẳng hạn như cảm biến gửi giá trị mới hoặc phản hồi mạng.

Mặc dù trạng thái của ứng dụng cung cấp mô tả về nội dung sẽ hiển thị trong giao diện người dùng, nhưng sự kiện là cơ chế thay đổi của trạng thái, dẫn đến các thay đổi đối với giao diện người dùng.

Sự kiện sẽ thông báo cho một phần của chương trình là có điều gì đó đã xảy ra. Tất cả ứng dụng Android đều có một vòng lặp cập nhật giao diện người dùng cốt lõi như bên dưới:

f415ca9336d83142.png

  • Sự kiện – Một sự kiện do người dùng hoặc một phần khác của chương trình tạo ra.
  • Trạng thái cập nhật – Trình xử lý sự kiện thay đổi trạng thái mà giao diện người dùng sử dụng.
  • Trạng thái hiển thị – Giao diện người dùng được cập nhật để hiển thị trạng thái mới.

Quản lý trạng thái trong Compose nghĩa là hiểu được cách các trạng thái và sự kiện tương tác với nhau.

Bây giờ, hãy thêm nút này để người dùng có thể sửa đổi trạng thái bằng cách thêm nhiều ly nước.

Chuyển đến hàm có khả năng kết hợp WaterCounter để thêm Button bên dưới nhãn Text. Column sẽ giúp bạn căn chỉnh Text theo chiều dọc với thành phần kết hợp Button. Bạn có thể di chuyển khoảng đệm bên ngoài vào thành phần kết hợp Column, sau đó thêm một số khoảng đệm bổ sung vào đầu Button để nội dung đó được tách khỏi Văn bản.

Hàm có khả năng kết hợp Button sẽ nhận được một hàm lambda onClick – đây là sự kiện xảy ra khi người dùng nhấp vào nút này. Bạn sẽ thấy các ví dụ khác về hàm lambda ở những phần sau.

Hãy thay đổi count thành var thay vì val để nó có thể thay đổi.

import androidx.compose.material3.Button
import androidx.compose.foundation.layout.Column

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count = 0
       Text("You've had $count glasses.")
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Khi chạy ứng dụng và nhấp vào nút, bạn sẽ nhận thấy rằng không có gì xảy ra. Việc đặt một giá trị khác cho biến count sẽ không làm cho Compose phát hiện biến này là một thay đổi về trạng thái, nên sẽ không có gì xảy ra. Điều này là do bạn chưa yêu cầu Compose vẽ lại màn hình (tức là "kết hợp lại" hàm có khả năng kết hợp) khi trạng thái thay đổi. Bạn sẽ khắc phục tình trạng này trong bước tiếp theo.

e4dfc3bef967e0a1.gif

5. Bộ nhớ trong hàm có khả năng kết hợp

Ứng dụng Compose biến đổi dữ liệu thành giao diện người dùng bằng cách gọi các hàm có khả năng kết hợp. Chúng tôi gọi Cấu trúc là nội dung mô tả về giao diện người dùng do Compose tạo ra khi thực thi các thành phần kết hợp. Nếu có thay đổi về trạng thái thì Compose sẽ thực thi lại các hàm có khả năng kết hợp bị ảnh hưởng với trạng thái mới, tạo ra một giao diện người dùng cập nhật. Đây gọi là quá trình kết hợp lại. Compose cũng xem xét những dữ liệu cần thiết cho một thành phần kết hợp riêng lẻ, để nó chỉ cần kết hợp lại các thành phần có dữ liệu đã thay đổi và bỏ qua những thành phần không bị ảnh hưởng.

Để làm điều này, Compose cần biết trạng thái cần theo dõi, để có thể lên lịch kết hợp lại khi nhận được bản cập nhật.

Compose có một hệ thống theo dõi trạng thái đặc biệt để lên lịch kết hợp lại cho bất cứ thành phần kết hợp nào đọc một trạng thái cụ thể. Điều này sẽ chi tiết hoá Compose và chỉ kết hợp lại các hàm có khả năng kết hợp cần thay đổi, chứ không phải toàn bộ giao diện người dùng. Việc này được thực hiện bằng cách không chỉ theo dõi "ghi" (nghĩa là thay đổi trạng thái), mà còn "đọc" trạng thái đó.

Sử dụng các loại StateMutableState trong Compose để khiến trạng thái có thể quan sát được bằng Compose.

Compose theo dõi từng thành phần kết hợp đọc thuộc tính value của trạng thái và kích hoạt quá trình kết hợp lại khi value thay đổi. Bạn có thể dùng hàm mutableStateOf để tạo một MutableState có thể quan sát được. Hàm này nhận giá trị ban đầu dưới dạng tham số được gói trong đối tượng State, sau đó giúp value có thể quan sát được.

Cập nhật thành phần kết hợp WaterCounter để count sử dụng API mutableStateOf với 0 làm giá trị ban đầu. Khi mutableStateOf trả về một loại MutableState, bạn có thể cập nhật value để cập nhật trạng thái, và Compose sẽ kích hoạt quá trình kết hợp lại các hàm đó khi value được đọc.

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       // Changes to count are now tracked by Compose
       val count: MutableState<Int> = mutableStateOf(0)

       Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Như đã đề cập trước đó, mọi thay đổi đối với count sẽ lên lịch kết hợp lại mọi hàm có khả năng kết hợp và tự động đọc value của count. Trong trường hợp này, WaterCounter sẽ được kết hợp lại mỗi khi người dùng nhấp vào nút này.

Nếu chạy ứng dụng ngay thì bạn sẽ nhận thấy chưa có điều gì xảy ra!

e4dfc3bef967e0a1.gif

Việc lên lịch kết hợp lại đang hoạt động tốt. Tuy nhiên, khi quá trình kết hợp lại xảy ra, biến count được khởi tạo sẽ trở về 0, vậy nên chúng ta cần giải pháp nào đó để lưu giữ giá trị này trên các quá trình kết hợp lại.

Để làm được điều này, chúng ta có thể dùng hàm cùng dòng có thể kết hợp remember. Cấu trúc lưu giữ giá trị do remember tính toán trong quá trình kết hợp ban đầu. Giá trị đã lưu trữ được giữ lại qua các lần kết hợp lại.

Thường thì remembermutableStateOf được dùng cùng nhau trong các hàm có khả năng kết hợp.

Có một vài cách tương đương để viết mã này như minh hoạ trong tài liệu về Trạng thái Compose.

Sửa đổi WaterCounter, bao quanh lệnh gọi đến mutableStateOf bằng hàm có khả năng kết hợp cùng dòng remember:

import androidx.compose.runtime.remember

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        val count: MutableState<Int> = remember { mutableStateOf(0) }
        Text("You've had ${count.value} glasses.")
        Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
            Text("Add one")
        }
    }
}

Ngoài ra, chúng ta có thể đơn giản hoá việc sử dụng count bằng cách dùng các thuộc tính được uỷ quyền của Kotlin.

Bạn có thể sử dụng từ khoá by để xác định count dưới dạng một var. Việc thêm lệnh nhập getter và setter của phần tử uỷ quyền cho phép chúng ta đọc và thay đổi count một cách gián tiếp mà không cần tham chiếu rõ ràng đến thuộc tính value của MutableState mọi lần.

WaterCounter hiện sẽ có dạng như sau:

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

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       Text("You've had $count glasses.")
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Bạn nên chọn cú pháp tạo ra mã dễ đọc nhất trong thành phần kết hợp mà bạn đang viết.

Giờ chúng ta sẽ xem lại những gì đã làm được nhé:

  • Đã xác định một biến theo thời gian được gọi là count.
  • Đã tạo một màn hình hiển thị văn bản để cho người dùng biết số mà chúng ta đã nhớ.
  • Đã thêm một nút làm tăng số lượng mà chúng ta đã nhớ bất cứ khi nào nó được nhấp vào.

Sự sắp xếp này tạo thành một vòng hồi tiếp luồng dữ liệu với người dùng:

  • Giao diện người dùng sẽ hiển thị trạng thái cho người dùng (số lượng hiện tại được hiển thị dưới dạng văn bản).
  • Người dùng tạo các sự kiện được kết hợp với trạng thái hiện có để tạo ra trạng thái mới (việc nhấp vào nút sẽ thêm một sự kiện vào số lượng hiện tại)

Bộ đếm của bạn đã sẵn sàng và hoạt động!

a9d78ead2c8362b6.gif

6. Giao diện người dùng hướng trạng thái

Compose là một khung giao diện người dùng khai báo. Thay vì xoá các thành phần giao diện người dùng hoặc thay đổi chế độ hiển thị khi trạng thái thay đổi, chúng ta sẽ mô tả giao diện người dùng trông như thế nào trong các điều kiện cụ thể của trạng thái. Do một quá trình kết hợp lại đang được gọi và giao diện người dùng được cập nhật, các thành phần kết hợp có thể được nhập hoặc rời khỏi Cấu trúc.

7d3509d136280b6c.png

Phương pháp này giúp bạn dễ dàng cập nhật chế độ xem theo cách thủ công như áp dụng với hệ thống Chế độ xem. Ngoài ra, chế độ xem này cũng ít gặp lỗi hơn vì bạn không thể quên cập nhật chế độ xem dựa trên trạng thái mới do chế độ này tự động diễn ra.

Nếu một hàm có khả năng kết hợp được gọi trong cấu trúc ban đầu, hoặc trong các thành phần kết hợp lại, thì hàm đó trong Cấu trúc. Một hàm có khả năng kết hợp không được gọi (ví dụ: vì hàm được gọi bên trong câu lệnh if và điều kiện này không được đáp ứng) sẽ không có trong Cấu trúc.

Bạn có thể tìm hiểu thêm về vòng đời của các thành phần kết hợp trong tài liệu.

Đầu ra của Cấu trúc là một cấu trúc dạng cây mô tả giao diện người dùng.

Bạn có thể kiểm tra bố cục ứng dụng do Compose tạo bằng cách sử dụng công cụ Layout Inspector của Android Studio, đây cũng là việc tiếp theo bạn sẽ thực hiện.

Để chứng minh điều này, hãy sửa đổi mã để hiển thị giao diện người dùng dựa trên trạng thái. Mở WaterCounter và hiển thị Text nếu count lớn hơn 0:

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }

       if (count > 0) {
           // This text is present if the button has been clicked
           // at least once; absent otherwise
           Text("You've had $count glasses.")
       }
       Button(onClick = { count++ }, Modifier.padding(top = 8.dp)) {
           Text("Add one")
       }
   }
}

Chạy ứng dụng và mở công cụ Layout Inspector của Android Studio bằng cách chuyển đến Tools (Công cụ) > Layout Inspector (Trình kiểm tra Bố cục).

Bạn sẽ thấy một màn hình chia đôi: cây thành phần ở bên trái và bản xem trước ứng dụng ở bên phải.

Nhấn vào phần tử gốc BasicStateCodelabTheme ở bên trái màn hình để di chuyển đến cây. Mở rộng toàn bộ cây thành phần bằng cách nhấp vào nút Expand all (Mở rộng tất cả).

Nhấp vào một phần tử trong màn hình ở bên phải để chuyển đến phần tử tương ứng của cây.

677bc0a178670de8.png

Nếu bạn nhấn nút Add one (Thêm một) trên ứng dụng:

  • Số lượng sẽ tăng lên 1 và trạng thái thay đổi.
  • Quá trình kết hợp lại được gọi.
  • Màn hình được kết hợp lại với các thành phần mới.

Khi kiểm tra cây thành phần bằng công cụ Layout Inspector của Android Studio, bạn cũng sẽ thấy thành phần kết hợp Text:

1f8e05f6497ec35f.png

Trạng thái sẽ điều khiển các phần tử có trong giao diện người dùng tại một thời điểm nhất định.

Các phần khác nhau của giao diện người dùng có thể phụ thuộc vào cùng một trạng thái. Sửa đổi Button để nó được bật cho đến khi count là 10, sau đó tắt (và bạn đã đạt được mục tiêu của mình trong ngày). Hãy dùng tham số enabled của Button để thực hiện việc này.

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    ...
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
    ...
}

Chạy ứng dụng ngay. Các thay đổi đối với count trạng thái sẽ xác định liệu có cho thấy Text hay không và liệu Button được bật hay tắt.

1a8f4095e384ba01.gif

7. Ghi nhớ trong Cấu trúc

remember lưu trữ các đối tượng trong Cấu trúc và quên đối tượng nếu vị trí nguồn nơi remember được gọi không được gọi lại trong quá trình kết hợp lại.

Để trực quan hoá hành vi này, bạn sẽ triển khai chức năng sau đây trong ứng dụng: khi người dùng đã uống ít nhất một ly nước, ứng dụng cho thấy một nhiệm vụ chăm sóc sức khoẻ mà người dùng cần thực hiện cũng như cho phép họ đóng nhiệm vụ đó. Vì thành phần kết hợp phải có kích thước nhỏ và có thể sử dụng lại, hãy tạo một thành phần kết hợp mới tên là WellnessTaskItem để cho thấy nhiệm vụ chăm sóc sức khoẻ đó dựa trên chuỗi nhận được dưới dạng tham số, cùng với nút biểu tượng Đóng.

Tạo tệp mới WellnessTaskItem.kt và thêm vào mã sau. Bạn sẽ sử dụng các hàm có khả năng kết hợp này sau trong lớp học lập trình này.

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

@Composable
fun WellnessTaskItem(
    taskName: String,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier.weight(1f).padding(start = 16.dp),
            text = taskName
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}

Hàm WellnessTaskItem nhận được nội dung mô tả tác vụ và hàm lambda onClose (cũng giống như thành phần kết hợp Button tích hợp sẵn sẽ nhận được onClick).

WellnessTaskItem sẽ có dạng như sau:

6e8b72a529e8dedd.png

Để cải thiện ứng dụng bằng việc bổ sung thêm tính năng, hãy cập nhật WaterCounter để hiển thị WellnessTaskItem khi count > 0.

Khi count lớn hơn 0, hãy xác định một biến showTask giúp xác định xem có hiển thị WellnessTaskItem hay không và khởi chạy biến đó thành giá trị true.

Thêm câu lệnh if mới để hiển thị WellnessTaskItem nếu showTask là giá trị true. Sử dụng các API bạn đã tìm hiểu ở những phần trước để đảm bảo giá trị showTask vẫn tồn tại sau khi kết hợp lại.

@Composable
fun WaterCounter() {
   Column(modifier = Modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count > 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { },
                   taskName = "Have you taken your 15 minute walk today?"
               )
           }
           Text("You've had $count glasses.")
       }

       Button(onClick = { count++ }, enabled = count < 10) {
           Text("Add one")
       }
   }
}

Dùng hàm lambda onClose của WellnessTaskItem để khi nhấn nút X, biến showTask sẽ thay đổi thành false và tác vụ không còn xuất hiện nữa.

   ...
   WellnessTaskItem(
      onClose = { showTask = false },
      taskName = "Have you taken your 15 minute walk today?"
   )
   ...

Tiếp theo, hãy thêm Button mới có nội dung "Clear water count" (Xoá lượng nước) rồi đặt bên cạnh nút "Add one" (Thêm một) Button. Row có thể giúp căn chỉnh hai nút. Bạn cũng có thể thêm một số khoảng đệm vào Row. Khi nhấn nút "Clear water count" (Xoá lượng nước), biến count được đặt lại về 0.

Hàm WaterCounter đã hoàn tất sẽ có dạng như sau.

import androidx.compose.foundation.layout.Row

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       var count by remember { mutableStateOf(0) }
       if (count > 0) {
           var showTask by remember { mutableStateOf(true) }
           if (showTask) {
               WellnessTaskItem(
                   onClose = { showTask = false },
                   taskName = "Have you taken your 15 minute walk today?"
               )
           }
           Text("You've had $count glasses.")
       }

       Row(Modifier.padding(top = 8.dp)) {
           Button(onClick = { count++ }, enabled = count < 10) {
               Text("Add one")
           }
           Button(
               onClick = { count = 0 },
               Modifier.padding(start = 8.dp)) {
                   Text("Clear water count")
           }
       }
   }
}

Khi bạn chạy ứng dụng, màn hình sẽ hiển thị trạng thái ban đầu:

Cây sơ đồ thành phần cho thấy trạng thái ban đầu của ứng dụng là 0

Ở bên phải, chúng tôi có một phiên bản đơn giản của cây thành phần, giúp bạn phân tích những gì đang xảy ra khi trạng thái thay đổi. countshowTask là các giá trị được ghi nhớ.

Giờ bạn có thể làm theo các bước sau trong ứng dụng:

  • Nhấn vào nút Thêm một. Việc này làm tăng count (dẫn đến quá trình kết hợp lại) và khiến cả WellnessTaskItem lẫn bộ đếm Text bắt đầu hiển thị.

Cây sơ đồ thành phần cho thấy sự thay đổi trạng thái, khi người dùng nhấp vào nút Add one (Thêm một), Text (Văn bản) kèm theo lưu ý sẽ xuất hiện và Text có số lượng ly nước cũng sẽ xuất hiện.

865af0485f205c28.png

  • Nhấn vào X của thành phần WellnessTaskItem (dẫn tới một quá trình kết hợp lại khác). showTask hiện đang là giá trị false, nghĩa là WellnessTaskItem không còn được hiển thị nữa.

Cây sơ đồ thành phần cho thấy khi người dùng nhấp vào nút đóng, thành phần kết hợp tác vụ sẽ biến mất.

82b5dadce9cca927.png

  • Nhấn vào nút Add one (Thêm một) (một quá trình kết hợp lại khác). showTask ghi nhớ bạn đã đóng WellnessTaskItem trong các lần tái cấu trúc tiếp theo nếu bạn tiếp tục thêm số ly nước.

  • Nhấn nút Xoá lượng nước để đặt lại count về 0 và tạo thành phần kết hợp lại. Text hiển thị count và mọi mã liên quan đến WellnessTaskItem sẽ không được gọi và rời khỏi Cấu trúc.

ae993e6ddc0d654a.png

  • showTask bị bỏ qua vì vị trí mã (nơi hàm ghi nhớ showTask được gọi) đã không được gọi. Bạn đang quay lại bước đầu tiên.

  • Nhấn nút Add one (Thêm một) để tạo count lớn hơn 0 (kết hợp lại).

7624eed0848a145c.png

  • Cấu trúc WellnessTaskItem sẽ xuất hiện lại vì giá trị showTask trước đó đã bị quên khi rời khỏi Cấu trúc nêu trên.

Điều gì sẽ xảy ra nếu chúng ta yêu cầu showTask vẫn tồn tại sau khi count quay về 0, lâu hơn mức mà remember cho phép (nghĩa là ngay cả khi vị trí mã – nơi remember được gọi – không được gọi trong quá trình kết hợp lại)? Chúng ta sẽ tìm hiểu cách khắc phục những trường hợp này và nhiều ví dụ khác trong các phần tiếp theo.

Bây giờ, bạn đã hiểu cách đặt lại giao diện người dùng và trạng thái khi thoát khỏi thành phần Compose, hãy xoá mã của bạn rồi quay lại WaterCounter bạn đã có ở đầu phần này:

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
    Column(modifier = modifier.padding(16.dp)) {
        var count by remember { mutableStateOf(0) }
        if (count > 0) {
            Text("You've had $count glasses.")
        }
        Button(onClick = { count++ }, Modifier.padding(top = 8.dp), enabled = count < 10) {
            Text("Add one")
        }
    }
}

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

Chạy ứng dụng, thêm một vài ly nước vào bộ đếm rồi xoay thiết bị. Đảm bảo bạn đã bật chế độ Tự động xoay của thiết bị.

Do Activity (Hoạt động) được tạo lại sau khi thay đổi cấu hình (trong trường hợp này là hướng) nên trạng thái lưu đã bị quên: bộ đếm sẽ biến mất khi quay về 0.

2c1134ad78e4b68a.gif

Điều tương tự cũng xảy ra nếu bạn thay đổi ngôn ngữ, chuyển đổi giữa chế độ tối và sáng hoặc thực hiện bất cứ thay đổi khác về cấu hình khiến Android tạo lại Activity (Hoạt động) đang chạy.

Mặc dù remember giúp bạn giữ lại trạng thái qua các lần kết hợp lại, nhưng trạng thái này không được giữ lại khi bạn thay đổi cấu hình. Để thực hiện việc này, bạn phải sử dụng rememberSaveable thay vì remember.

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. Để biết thêm thông tin về cách Khôi phục trạng thái trong Compose, vui lòng xem tài liệu.

Trong WaterCounter, hãy thay thế remember bằng rememberSaveable:

import androidx.compose.runtime.saveable.rememberSaveable

@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
        ...
        var count by rememberSaveable { mutableStateOf(0) }
        ...
}

Chạy ứng dụng ngay và thử thực hiện một số thay đổi về cấu hình. Bạn sẽ thấy bộ đếm được lưu đúng cách.

bf2e1634eff47697.gif

Tạo lại hoạt động chỉ là một trong những trường hợp sử dụng của rememberSaveable. Chúng ta sẽ tìm hiểu một trường hợp sử dụng khác sau khi xử lý các danh sách.

Cân nhắc xem nên sử dụng remember hay rememberSaveable tuỳ thuộc vào trạng thái và nhu cầu trải nghiệm người dùng của ứng dụng.

9. Chuyển trạng thái lên trên (state hoisting)

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. Điều này có thể hữu ích trong trường hợp phương thức 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.

Các thành phần kết hợp không có trạng thái nào được gọi là thành phần kết hợp không có trạng thái (stateless composable). Một cách dễ dàng để tạo thành phần kết hợp 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.

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 phương thức 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 giá trị thay đổi, trong đó T là giá trị mới

nơi giá trị này thể hiện cho bất kỳ trạng thái nào có thể sửa đổi.

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.
  • 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.
  • Có thể chắn (interceptable): Phương thức 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.
  • Decoupled (tách riêng): Trạng thái cho hàm có khả năng kết hợp không có trạng thái, có thể được lưu trữ ở bất cứ đâu. Ví dụ như trong ViewModel.

Hãy cố gắng triển khai trạng thái này cho WaterCounter để có thể hưởng lợi từ tất cả các thuộc tính trên.

Có trạng thái so với Không có trạng thái

Khi tất cả trạng thái có thể được trích xuất từ một hàm có khả năng kết hợp, hàm đó được gọi là hàm không có trạng thái.

Tái cấu trúc thành phần kết hợp WaterCounter bằng cách chia thành hai phần: Bộ đếm có trạng thái và không có trạng thái.

Vai trò của StatelessCounter là hiển thị count và gọi một hàm khi bạn tăng count. Để thực hiện việc này, hãy làm theo mẫu được mô tả ở trên và chuyển trạng thái, count (dưới dạng tham số đến hàm có khả năng kết hợp) và hàm lambda (onIncrement), được gọi khi trạng thái cần được tăng lên. StatelessCounter sẽ có dạng như sau:

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
   Column(modifier = modifier.padding(16.dp)) {
       if (count > 0) {
           Text("You've had $count glasses.")
       }
       Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
           Text("Add one")
       }
   }
}

StatefulCounter sở hữu trạng thái này. Nghĩa là nó giữ trạng thái count và sửa đổi khi gọi hàm StatelessCounter.

@Composable
fun StatefulCounter(modifier: Modifier = Modifier) {
   var count by rememberSaveable { mutableStateOf(0) }
   StatelessCounter(count, { count++ }, modifier)
}

Tốt lắm! Bạn đã nâng count từ StatelessCounter lên StatefulCounter.

Bạn có thể cắm vào ứng dụng của mình và cập nhật WellnessScreen bằng StatefulCounter:

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   StatefulCounter(modifier)
}

Như đã đề cập, việc di chuyển trạng thái lên trên có một số lợi ích. Chúng ta sẽ khám phá các biến thể của mã này để giải thích một số biến thể đó, bạn không cần phải sao chép các đoạn mã sau trong ứng dụng.

  1. Thành phần kết hợp không có trạng thái hiện có thể được sử dụng lại. Hãy lấy ví dụ bên dưới.

Để đếm các ly nước và nước ép, bạn nhớ waterCountjuiceCount, nhưng hãy sử dụng cùng một hàm có khả năng kết hợp StatelessCounter để hiển thị hai trạng thái độc lập khác nhau.

@Composable
fun StatefulCounter() {
    var waterCount by remember { mutableStateOf(0) }

    var juiceCount by remember { mutableStateOf(0) }

    StatelessCounter(waterCount, { waterCount++ })
    StatelessCounter(juiceCount, { juiceCount++ })
}

8211bd9e0a4c5db2.png

Nếu juiceCount được sửa đổi, thì StatefulCounter sẽ được kết hợp lại. Trong quá trình kết hợp lại, Compose sẽ xác định những hàm có vai trò đọc juiceCount và kích hoạt quá trình kết hợp lại chỉ với những hàm đó.

2cb0dcdbe75dcfbf.png

Khi người dùng nhấn để tăngjuiceCount .StatefulCounter kết hợp lại và do đó, StatelessCounter sẽ có nội dung juiceCount . Nhưng StatelessCounter đọc waterCount thì không được kết hợp lại.

7fe6ee3d2886abd0.png

  1. Hàm có khả năng kết hợp có trạng thái có thể cung cấp cùng một trạng thái cho nhiều hàm có khả năng kết hợp.
@Composable
fun StatefulCounter() {
   var count by remember { mutableStateOf(0) }

   StatelessCounter(count, { count++ })
   AnotherStatelessMethod(count, { count *= 2 })
}

Trong trường hợp này, nếu số lượng được cập nhật bởi StatelessCounter hoặc AnotherStatelessMethod, mọi hàm sẽ được kết hợp lại như dự kiến.

Vì trạng thái được chuyển lên trên có thể chia sẻ được, bạn phải nhớ chỉ chuyển trạng thái mà các thành phần kết hợp cần để tránh các thành phần kết hợp lại không cần thiết cũng như tăng khả năng tái sử dụng.

Để đọc thêm về trạng thái và việc chuyển trạng thái lên trên, vui lòng xem tài liệu về Trạng thái Compose.

10. Xử lý các danh sách

Tiếp theo, hãy thêm tính năng thứ hai cho ứng dụng của bạn là danh sách các nhiệm vụ về sức khoẻ. Bạn có thể thực hiện hai hành động với các mục trong danh sách:

  • Đánh dấu các mục trong danh sách để cho biết nhiệm vụ đã hoàn thành.
  • Xoá nhiệm vụ khỏi danh sách mà bạn không muốn hoàn thành.

Thiết lập

  1. Trước tiên, hãy sửa đổi mục danh sách. Bạn có thể sử dụng lại WellnessTaskItem ở phần Ghi nhớ trong Cấu trúc và cập nhật thành phần này để chứa Checkbox. Hãy đảm bảo bạn nâng trạng thái checked và lệnh gọi lại onCheckedChange để làm cho hàm không có trạng thái.

a0f8724cfd33cb10.png

Thành phần kết hợp WellnessTaskItem cho phần này sẽ có dạng như sau:

import androidx.compose.material3.Checkbox

@Composable
fun WellnessTaskItem(
    taskName: String,
    checked: Boolean,
    onCheckedChange: (Boolean) -> Unit,
    onClose: () -> Unit,
    modifier: Modifier = Modifier
) {
    Row(
        modifier = modifier, verticalAlignment = Alignment.CenterVertically
    ) {
        Text(
            modifier = Modifier
                .weight(1f)
                .padding(start = 16.dp),
            text = taskName
        )
        Checkbox(
            checked = checked,
            onCheckedChange = onCheckedChange
        )
        IconButton(onClick = onClose) {
            Icon(Icons.Filled.Close, contentDescription = "Close")
        }
    }
}
  1. Cũng trong tệp đó, hãy thêm một hàm có khả năng kết hợp WellnessTaskItem có trạng thái xác định một biến trạng thái checkedState, và chuyển biến đó vào phương thức không có trạng thái cùng tên. Đừng bận tâm về onClose hiện tại, bạn có thể truyền một hàm lambda trống.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember

@Composable
fun WellnessTaskItem(taskName: String, modifier: Modifier = Modifier) {
   var checkedState by remember { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = {}, // we will implement this later!
       modifier = modifier,
   )
}
  1. Tạo tệp WellnessTask.kt để mô hình hoá một tác vụ chứa mã nhận dạng và nhãn. Hãy xác định lớp đó dưới dạng một lớp dữ liệu.
data class WellnessTask(val id: Int, val label: String)
  1. Đối với danh sách tác vụ, hãy tạo một tệp mới có tên là WellnessTasksList.kt rồi thêm phương thức tạo ra một số dữ liệu giả:
fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

Lưu ý trong một ứng dụng thực tế, bạn sẽ nhận dữ liệu từ lớp dữ liệu.

  1. Trong WellnessTasksList.kt, hãy thêm một hàm có khả năng kết hợp tạo danh sách. Xác định LazyColumn và các mục trong phương thức danh sách mà bạn đã tạo. Vui lòng xem tài liệu về Danh sách nếu bạn cần trợ giúp.
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.runtime.remember

@Composable
fun WellnessTasksList(
    modifier: Modifier = Modifier,
    list: List<WellnessTask> = remember { getWellnessTasks() }
) {
    LazyColumn(
        modifier = modifier
    ) {
        items(list) { task ->
            WellnessTaskItem(taskName = task.label)
        }
    }
}
  1. Thêm danh sách này vào WellnessScreen. Sử dụng Column để giúp căn chỉnh danh sách theo chiều dọc với bộ đếm bạn đã có.
import androidx.compose.foundation.layout.Column

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()
       WellnessTasksList()
   }
}
  1. Chạy ứng dụng và dùng thử! Giờ thì bạn đã có thể kiểm tra các tác vụ nhưng không thể xoá chúng. Bạn sẽ triển khai tính năng đó ở phần sau.

f9cbc49c960fd24c.gif

Khôi phục trạng thái của mục trong LazyList

Vui lòng xem xét kỹ hơn một số nội dung trong thành phần kết hợp WellnessTaskItem.

checkedState thuộc về từng thành phần kết hợp WellnessTaskItem một cách độc lập, giống như một biến riêng. Khi checkedState thay đổi, chỉ có phiên bản WellnessTaskItem đó được kết hợp lại chứ không phải tất cả phiên bản WellnessTaskItem trong LazyColumn.

Hãy thử bằng cách làm theo các bước sau:

  1. Đánh dấu phần tử bất kỳ ở đầu danh sách này (ví dụ như các phần tử 1 và 2).
  2. Di chuyển xuống cuối danh sách để ra khỏi màn hình.
  3. Cuộn lại lên trên cùng các mục mà bạn đã chọn trước đó.
  4. Lưu ý bạn đã bỏ đánh dấu các hộp này.

Có một sự cố, như bạn đã thấy ở phần trước, khi một mục rời khỏi Cấu trúc, trạng thái được ghi nhớ sẽ bị bỏ qua. Các mục trên LazyColumn sẽ hoàn toàn rời khỏi Cấu trúc khi bạn di chuyển qua các mục đó và chúng không còn xuất hiện nữa.

a68b5473354d92df.gif

Bạn sẽ khắc phục vấn đề này bằng cách nào? Một lần nữa, hãy dùng rememberSaveable. Trạng thái mà bạn chọn sẽ vẫn tồn tại trong quá trình tạo lại hoạt động hoặc quy trình thông qua việc sử dụng cơ chế trạng thái của thực thể đã lưu. Nhờ cách kết hợp của rememberSaveable với LazyList, các mục của bạn vẫn có khả năng tồn tại khi rời khỏi Cấu trúc.

Bạn chỉ cần thay thế remember bằng rememberSaveable trong WellnessTaskItem trạng thái, và thế là xong:

import androidx.compose.runtime.saveable.rememberSaveable

var checkedState by rememberSaveable { mutableStateOf(false) }

85796fb49cf5dd16.gif

Các mẫu phổ biến trong Compose

Lưu ý việc triển khai LazyColumn:

@Composable
fun LazyColumn(
...
    state: LazyListState = rememberLazyListState(),
...

Hàm có khả năng kết hợp rememberLazyListState tạo trạng thái ban đầu cho danh sách bằng rememberSaveable. Khi Hoạt động được tạo lại, trạng thái cuộn được duy trì mà bạn không cần phải lập trình.

Nhiều ứng dụng cần phản ứng và tuân theo vị trí cuộn, thay đổi bố cục mục cũng như các sự kiện khác liên quan đến trạng thái của danh sách. Các thành phần Lazy, chẳng hạn như LazyColumn hoặc LazyRow, hỗ trợ trường hợp sử dụng này thông qua việc nâng cấp LazyListState. Bạn có thể tìm hiểu thêm về mẫu này trong tài liệu về trạng thái trong danh sách.

Việc lấy tham số trạng thái có giá trị mặc định do hàm rememberX công khai cung cấp là một mẫu hình phổ biến trong các hàm có khả năng kết hợp được tích hợp sẵn. Bạn có thể tìm thấy một ví dụ khác trong BottomSheetScaffold, theo đó trạng thái được di chuyển lên trên bằng rememberBottomSheetScaffoldState.

11. Danh sách MutableList có thể quan sát được

Tiếp theo, để thêm hành vi xoá một tác vụ khỏi danh sách của chúng tôi, trước tiên, bạn cần đặt danh sách đó thành một danh sách có thể thay đổi.

Bạn không sử dụng được đối tượng có thể thay đổi cho mục này, chẳng hạn như ArrayList<T> hoặc mutableListOf,. Loại đối tượng này sẽ không thông báo cho Compose về các mục trong danh sách đã thay đổi và lên lịch kết hợp lại giao diện người dùng. Bạn cần một API khác.

Bạn cần tạo một thực thể MutableList có thể quan sát được bằng Compose. Cấu trúc này cho phép Compose theo dõi các thay đổi để tái cấu trúc giao diện người dùng khi các mục được thêm vào hoặc bị xoá khỏi danh sách.

Bắt đầu bằng cách xác định MutableList có thể quan sát được. Hàm mở rộng toMutableStateList() là cách tạo MutableList có thể quan sát được từ Collection có thể hoặc không thể thay đổi ban đầu, chẳng hạn như List.

Ngoài ra, bạn cũng có thể sử dụng phương thức trạng thái ban đầu mutableStateListOf để tạo MutableList có thể quan sát được, sau đó thêm các phần tử cho trạng thái ban đầu.

  1. Mở tệp WellnessScreen.kt. Hãy di chuyển phương thức getWellnessTasks sang tệp này để có thể sử dụng. Tạo danh sách bằng cách gọi getWellnessTasks() trước rồi sử dụng hàm mở rộng toMutableStateList mà bạn đã tìm hiểu trước đó.
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()

       val list = remember { getWellnessTasks().toMutableStateList() }
       WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
  1. Hãy sửa đổi hàm có khả năng kết hợp WellnessTasksList bằng cách xoá giá trị mặc định của danh sách, vì danh sách được chuyển lên cấp màn hình. Thêm một tham số hàm lambda mới onCloseTask (nhận WellnessTask để xoá ). Truyền onCloseTask vào WellnessTaskItem.

Bạn cần thực hiện một thay đổi nữa. Phương thức items nhận được tham số key. Theo mặc định, trạng thái của mỗi mục được khoá dựa vào vị trí của mục trong danh sách.

Trong danh sách có thể thay đổi, việc này sẽ gây ra sự cố khi tập dữ liệu thay đổi, vì các mục thay đổi vị trí sẽ mất bất kỳ trạng thái nào đã được ghi nhớ.

Bạn có thể dễ dàng khắc phục vấn đề này bằng cách sử dụng id của từng WellnessTaskItem làm khoá cho mỗi mục.

Để tìm hiểu thêm về nội dung khoá mục trong một danh sách, vui lòng xem tài liệu.

WellnessTasksList sẽ có dạng như sau:

@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(modifier = modifier) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
       }
   }
}
  1. Sửa đổi WellnessTaskItem: thêm hàm lambda onClose dưới dạng tham số vào WellnessTaskItem trạng thái và gọi hàm đó.
@Composable
fun WellnessTaskItem(
   taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
   var checkedState by rememberSaveable { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = onClose,
       modifier = modifier,
   )
}

Tốt lắm! Chức năng đã hoàn tất và xoá được một mục khỏi danh sách hoạt động.

Nếu bạn nhấp vào dấu X trong mỗi hàng, thì các sự kiện sẽ chuyển đến danh sách sở hữu trạng thái, xoá mục khỏi danh sách và khiến Compose kết hợp lại màn hình.

47f4a64c7e9a5083.png

Nếu cố gắng sử dụng rememberSaveable() để lưu trữ danh sách trong WellnessScreen thì bạn sẽ nhận được một ngoại lệ cho thời gian chạy:

Lỗi này cho biết bạn phải cung cấp trình lưu tuỳ chỉnh. Tuy nhiên, bạn không nên sử dụng rememberSaveable để lưu trữ một lượng lớn dữ liệu hoặc cấu trúc dữ liệu phức tạp đòi hỏi quá trình chuyển đổi tuần tự hoặc huỷ chuyển đổi tuần tự dài.

Các quy tắc tương tự áp dụng khi làm việc với onSaveInstanceState của Hoạt động; bạn có thể xem thêm thông tin trong tài liệu về Lưu trạng thái giao diện người dùng. Nếu làm như vậy, bạn cần có một cơ chế lưu trữ thay thế. Bạn có thể tìm hiểu thêm về nhiều phương án giúp duy trì trạng thái giao diện người dùng trong tài liệu.

Tiếp theo, chúng ta sẽ xem vai trò của ViewModel là phần tử sở hữu trạng thái của ứng dụng.

12. Trạng thái trong ViewModel

Màn hình hoặc trạng thái giao diện người dùng cho biết nội dung sẽ hiển thị trên màn hình (ví dụ như danh sách tác vụ). 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..

Mặc dù trạng thái giao diện người dùng mô tả nội dung xuất hiện trên màn hình, nhưng logic của ứng dụng lại mô tả cách ứng dụng hoạt động và sẽ phản ứng với các thay đổi về trạng thái. Có hai loại logic: logic hành vi trên giao diện người dùng hoặc logic giao diện người dùng, và logic kinh doanh.

  • Logic giao diện người dùng liên quan đến các thay đổi về trạng thái cách hiển thị trên màn hình (ví dụ như logic điều hướng hoặc hiển thị thanh thông báo nhanh).
  • Logic kinh doanh là những việc nên làm trước những thay đổi về trạng thái (ví dụ: thanh toán hoặc lưu trữ lựa chọn ưu tiê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.

ViewModel cho biết trạng thái của giao diện người dùng cũng như quyền tiếp cận logic nghiệp vụ trong các lớp khác của ứng dụng. Ngoài ra, ViewModel giữ lại các thay đổi về cấu hình nên chúng có thời gian tồn tại lâu hơn so với Cấu trúc. 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, mảnh hoặc đích đến của Biểu đồ điều hướng) nếu bạn đang sử dụng tính năng Điều hướng Compose.

Để tìm hiểu thêm về cấu trúc và lớp giao diện người dùng, vui lòng xem tài liệu về lớp giao diện người dùng.

Di chuyển danh sách và xoá phương thức

Mặc dù các bước trước cho bạn biết cách quản lý trạng thái trực tiếp trong các Hàm có khả năng kết hợp, nhưng bạn nên tách biệt logic giao diện người dùng và logic nghiệp vụ với trạng thái giao diện người dùng rồi di chuyển sang một ViewModel.

Hãy di chuyển trạng thái giao diện người dùng, danh sách sang ViewModel, đồng thời bắt đầu trích xuất logic nghiệp vụ vào đó.

  1. Tạo một tệp WellnessViewModel.kt để thêm lớp ViewModel.

Di chuyển getWellnessTasks() "nguồn dữ liệu" của bạn sang WellnessViewModel.

Xác định biến _tasks nội bộ, sử dụng toMutableStateList như bạn đã làm trước đây và cho thấy tasks dưới dạng danh sách, để không sửa đổi được thuộc tính này từ bên ngoài ViewModel.

Triển khai một hàm remove đơn giản có thể uỷ quyền cho hàm xoá tích hợp trong danh sách.

import androidx.compose.runtime.toMutableStateList
import androidx.lifecycle.ViewModel

class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List<WellnessTask>
        get() = _tasks

   fun remove(item: WellnessTask) {
       _tasks.remove(item)
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
  1. Chúng ta có thể truy cập ViewModel này từ bất kỳ thành phần kết hợp nào bằng cách gọi hàm viewModel().

Để dùng hàm này, hãy mở tệp app/build.gradle.kts, thêm thư viện sau đây rồi đồng bộ hoá các phần phụ thuộc mới trong Android Studio:

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")

Sử dụng phiên bản 2.6.2 đối với Android Studio Giraffe. Nếu không, hãy kiểm tra phiên bản mới nhất của thư viện tại đây.

  1. Mở WellnessScreen. Tạo bản sao ViewModel của wellnessViewModel bằng cách gọi viewModel() dưới dạng tham số của thành phần kết hợp Màn hình. Bản sao này có thể được thay thế khi thử nghiệm thành phần kết hợp và được nâng lên nếu cần. Cung cấp cho WellnessTasksList danh sách tác vụ cũng như xoá hàm khỏi lambda onCloseTask.
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCloseTask = { task -> wellnessViewModel.remove(task) })
   }
}

viewModel()trả về một chế độ ViewModel hiện có hoặc tạo một chế độ mới trong phạm vi nhất định. Thực thể ViewModel được giữ lại miễn là phạm vi vẫn hoạt động. Ví dụ như nếu sử dụng thành phần kết hợp trong một hoạt động, viewModel() sẽ trả về cùng một phiên bản cho đến khi hoạt động đó kết thúc hoặc quá trình kết thúc.

Chỉ vậy thôi! Bạn đã tích hợp ViewModel với một phần trạng thái và logic kinh doanh với màn hình của mình. Vì trạng thái được giữ bên ngoài Cấu trúc và do ViewModel lưu trữ, nên các thay đổi đối với danh sách sẽ vẫn tồn tại sau khi thay đổi cấu hình.

ViewModel sẽ không tự động duy trì trạng thái của ứng dụng trong bất cứ trường hợp nào (ví dụ: quá trình bị buộc tắt do hệ thống gây ra). Để biết thông tin chi tiết về cách lưu giữ trạng thái giao diện người dùng của ứng dụng, vui lòng xem tài liệu.

Di chuyển trạng thái đã đánh dấu

Tái cấu trúc gần đây nhất nghĩa là di chuyển trạng thái và logic đã đánh dấu sang ViewModel. Bằng cách này, mã sẽ đơn giản và dễ kiểm thử hơn nhờ tất cả trạng thái do ViewModel quản lý.

  1. Trước tiên, hãy sửa đổi lớp mô hình WellnessTask để lớp này có thể lưu trữ trạng thái đã đánh dấu và đặt giá trị mặc định là false.
data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)
  1. Trong ViewModel, hãy triển khai một phương thức changeTaskChecked nhận tác vụ cần sửa đổi với một giá trị mới cho trạng thái đã đánh dấu.
class WellnessViewModel : ViewModel() {
   ...
   fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
       _tasks.find { it.id == item.id }?.let { task ->
           task.checked = checked
       }
}
  1. Trong WellnessScreen, cung cấp hành vi cho onCheckedTask của danh sách bằng cách gọi phương thức changeTaskChecked trong ViewModel. Các hàm giờ đây sẽ có dạng như sau:
@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier, 
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCheckedTask = { task, checked ->
               wellnessViewModel.changeTaskChecked(task, checked)
           },
           onCloseTask = { task ->
               wellnessViewModel.remove(task)
           }
       )
   }
}
  1. Mở WellnessTasksList rồi thêm tham số hàm lambda onCheckedTask để bạn có thể truyền tham số đó xuống WellnessTaskItem.
@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCheckedTask: (WellnessTask, Boolean) -> Unit,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(
       modifier = modifier
   ) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(
               taskName = task.label,
               checked = task.checked,
               onCheckedChange = { checked -> onCheckedTask(task, checked) },
               onClose = { onCloseTask(task) }
           )
       }
   }
}
  1. Dọn dẹp tệp WellnessTaskItem.kt. Chúng ta không cần một phương thức trạng thái nữa, vì trạng thái CheckBox sẽ được chuyển lên cấp Danh sách. Tệp chỉ có hàm có khả năng kết hợp này:
@Composable
fun WellnessTaskItem(
   taskName: String,
   checked: Boolean,
   onCheckedChange: (Boolean) -> Unit,
   onClose: () -> Unit,
   modifier: Modifier = Modifier
) {
   Row(
       modifier = modifier, verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           modifier = Modifier
               .weight(1f)
               .padding(start = 16.dp),
           text = taskName
       )
       Checkbox(
           checked = checked,
           onCheckedChange = onCheckedChange
       )
       IconButton(onClick = onClose) {
           Icon(Icons.Filled.Close, contentDescription = "Close")
       }
   }
}
  1. Chạy ứng dụng rồi thử kiểm tra tác vụ bất kỳ. Xin lưu ý rằng chức năng kiểm tra tác vụ vẫn chưa hoạt động hiệu quả.

1d08ebcade1b9302.gif

Lý do là vì đối tượng mà Compose đang theo dõi cho MutableList là các thay đổi liên quan đến việc thêm và xoá phần tử. Vì vậy, chức năng xoá mới hoạt động. Nhưng chức năng này không nhận biết về các thay đổi trong giá trị của mục hàng (là checkedState trong trường hợp này), trừ phi bạn cũng yêu cầu theo dõi các giá trị đó.

Có hai cách để khắc phục vấn đề này:

  • Thay đổi lớp dữ liệu WellnessTask để checkedState trở thành MutableState<Boolean> thay vì Boolean, điều này khiến Compose theo dõi sự thay đổi về mục.
  • Sao chép mục bạn sắp thay đổi, xoá mục đó khỏi danh sách rồi thêm lại mục đó vào danh sách. Thao tác này giúp Compose theo dõi thay đổi đó.

Cả hai phương pháp đều có những ưu và nhược điểm. Ví dụ như tuỳ thuộc vào cách bạn đang triển khai danh sách, việc xoá và đọc phần tử có thể gây tốn kém.

Giả sử bạn muốn tránh các hoạt động danh sách có thể tốn kém và làm cho checkedState có thể quan sát được vì cách này hiệu quả hơn và tương thích với Compose hơn.

WellnessTask mới của bạn có thể có dạng như sau:

import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))

Như đã thấy trước đó, bạn có thể sử dụng các thuộc tính được uỷ quyền, giúp việc sử dụng biến checked đơn giản hơn cho trường hợp này.

Thay đổi WellnessTask thành một lớp thay vì lớp dữ liệu. Thiết lập để WellnessTask nhận một biến initialChecked có giá trị mặc định false trong hàm khởi tạo, sau đó, chúng ta có thể khởi tạo biến checked bằng phương thức ban đầu mutableStateOf rồi thiết lập initialChecked làm giá trị mặc định.

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

class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}

Vậy là xong! Giải pháp này khá hiệu quả, mà tất cả thay đổi vẫn tồn tại sau khi kết hợp lại và thay đổi cấu hình!

e7cc030cd7e8b66f.gif

Kiểm thử

Giờ đây, logic nghiệp vụ sẽ được tái cấu trúc thành ViewModel thay vì được kết hợp bên trong các hàm có thể kết hợp, việc kiểm thử đơn vị sẽ đơn giản hơn nhiều.

Bạn có thể sử dụng kiểm thử đo lường để xác minh hành vi chính xác của mã Compose cũng như trạng thái giao diện người dùng đang hoạt động đúng cách. Hãy cân nhắc tham gia lớp học lập trình Kiểm thử trong Compose để tìm hiểu cách kiểm thử giao diện người dùng trong Compose.

13. Xin chúc mừng

Tốt lắm! Bạn đã hoàn tất thành công lớp học lập trình này và tìm hiểu tất cả các API cơ bản để xử lý trạng thái trong ứng dụng Jetpack Compose!

Bạn đã tìm hiểu cách hoạt động của trạng thái và sự kiện để trích xuất các thành phần kết hợp không có trạng thái trong Compose, cũng như cách Compose sử dụng thông tin cập nhật trạng thái để thúc đẩy thay đổi trong giao diện người dùng.

Tiếp theo là gì?

Hãy tham khảo các lớp học lập trình khác trên Lộ trình học tập về Compose.

Ứng dụng mẫu

  • JetNews trình bày các phương pháp hay nhất được giải thích trong lớp học lập trình này.

Tài liệu khác

API tham chiếu