Khắc phục sự cố về độ ổn định

Khi gặp phải một lớp không ổn định gây ra vấn đề về hiệu suất, bạn nên làm cho lớp đó ổn định. Tài liệu này trình bày một số kỹ thuật bạn có thể sử dụng để thực hiện việc này.

Đặt lớp này là không thể thay đổi

Trước tiên, bạn nên cố gắng tạo một lớp không ổn định, hoàn toàn bất biến.

  • Không thể thay đổi: Cho biết một loại trong đó giá trị của bất kỳ thuộc tính nào không thể thay đổi sau khi tạo một thực thể của loại đó và tất cả các phương thức đều trong suốt.
    • Đảm bảo tất cả thuộc tính của lớp đều là val thay vì var và thuộc các loại không thể thay đổi.
    • Các loại nguyên gốc như String, IntFloat luôn không thể thay đổi.
    • Nếu không thể thực hiện việc này, bạn phải sử dụng trạng thái Compose cho mọi thuộc tính có thể thay đổi.
  • Ổn định: Cho biết loại có thể thay đổi. Môi trường thời gian chạy Compose sẽ không biết khi nào và liệu thuộc tính công khai hoặc hành vi của phương thức thuộc loại này có mang lại kết quả khác với lệnh gọi trước đó hay không.

Bộ sưu tập bất biến

Một lý do phổ biến khiến Compose coi một lớp không ổn định là các tập hợp. Như đã lưu ý trên trang Chẩn đoán các vấn đề về tính ổn định, trình biên dịch Compose không thể chắc chắn rằng các tập hợp như List, MapSet thực sự không thể thay đổi được và do đó đánh dấu chúng là không ổn định.

Để giải quyết vấn đề này, bạn có thể sử dụng các tập hợp không thể thay đổi. Trình biên dịch Compose hỗ trợ Bộ sưu tập bất biến Kotlinx. Các bộ sưu tập này được đảm bảo là không thể thay đổi và trình biên dịch Compose sẽ xử lý các bộ sưu tập này như vậy. Thư viện này vẫn đang trong giai đoạn thử nghiệm alpha, vì vậy, hãy chờ đón các thay đổi có thể xảy ra đối với API của thư viện này.

Hãy xem xét lại lớp không ổn định này trong hướng dẫn Chẩn đoán các vấn đề về tính ổn định:

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

Bạn có thể làm cho tags ổn định bằng cách sử dụng tập hợp bất biến. Trong lớp này, hãy thay đổi loại của tags thành ImmutableSet<String>:

data class Snack{
    …
    val tags: ImmutableSet<String> = persistentSetOf()
    …
}

Sau đó, tất cả tham số của lớp là không thể thay đổi và trình biên dịch Compose sẽ đánh dấu lớp đó là ổn định.

Chú thích bằng Stable hoặc Immutable

Một cách khả thi để giải quyết các vấn đề về độ ổn định là chú thích các lớp không ổn định bằng @Stable hoặc @Immutable.

Việc chú thích một lớp sẽ ghi đè những gì mà trình biên dịch sẽ dự đoán về lớp của bạn. Hàm này tương tự như toán tử !! trong Kotlin. Bạn nên thận trọng về cách sử dụng các chú thích này. Việc ghi đè hành vi của trình biên dịch có thể dẫn đến các lỗi không lường trước, chẳng hạn như thành phần kết hợp không kết hợp lại như bạn mong đợi.

Nếu có thể làm cho lớp trở nên ổn định mà không cần chú giải, bạn nên cố gắng đạt được độ ổn định theo cách đó.

Đoạn mã sau đây cung cấp một ví dụ tối thiểu về lớp dữ liệu được chú thích là không thể thay đổi:

@Immutable
data class Snack(
…
)

Cho dù bạn sử dụng chú giải @Immutable hay @Stable, trình biên dịch Compose sẽ đánh dấu lớp Snack là ổn định.

Lớp chú thích trong bộ sưu tập

Hãy cân nhắc một thành phần kết hợp chứa tham số thuộc loại List<Snack>:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  …
  unstable snacks: List<Snack>
  …
)

Ngay cả khi bạn chú giải Snack bằng @Immutable, trình biên dịch Compose vẫn đánh dấu tham số snacks trong HighlightedSnacks là không ổn định.

Tham số gặp phải vấn đề tương tự như lớp khi nói đến loại bộ sưu tập, trình biên dịch Compose luôn đánh dấu tham số thuộc kiểu List là không ổn định, ngay cả khi đó là một tập hợp các kiểu ổn định.

Bạn không thể đánh dấu một thông số riêng lẻ là ổn định và cũng không thể chú thích một thành phần kết hợp để luôn có thể bỏ qua. Có nhiều đường dẫn phía trước.

Có một số cách bạn có thể giải quyết vấn đề về bộ sưu tập không ổn định. Các tiểu mục sau đây trình bày những phương pháp tiếp cận khác nhau.

Tệp cấu hình

Nếu muốn tuân thủ hợp đồng về độ ổn định trong cơ sở mã của mình, thì bạn có thể chọn coi các bộ sưu tập Kotlin là ổn định bằng cách thêm kotlin.collections.* vào tệp cấu hình về độ ổn định.

Bộ sưu tập bất biến

Để đảm bảo an toàn về thời gian biên dịch của tính bất biến, bạn có thể sử dụng bộ sưu tập bất biến kotlinx thay vì List.

@Composable
private fun HighlightedSnacks(
    …
    snacks: ImmutableList<Snack>,
    …
)

Wrapper

Nếu không thể sử dụng bộ sưu tập bất biến, bạn có thể tạo bộ sưu tập của riêng mình. Để làm được như vậy, hãy gói List trong một lớp ổn định có chú giải. Tuỳ thuộc vào yêu cầu của bạn, trình bao bọc chung có thể là lựa chọn tốt nhất cho trường hợp này.

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

Sau đó, bạn có thể dùng loại tham số này làm kiểu tham số trong thành phần kết hợp.

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

Giải pháp

Sau khi thực hiện một trong hai phương pháp này, trình biên dịch Compose giờ đây sẽ đánh dấu Thành phần kết hợp HighlightedSnacks là cả skippablerestartable.

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

Trong quá trình kết hợp lại, Compose hiện có thể bỏ qua HighlightedSnacks nếu không có dữ liệu đầu vào nào thay đổi.

Tệp cấu hình độ ổn định

Kể từ Compose Compiler 1.5.5, bạn có thể cung cấp tệp cấu hình của các lớp được coi là ổn định tại thời điểm biên dịch. Điều này cho phép xem xét các lớp mà bạn không kiểm soát, chẳng hạn như các lớp thư viện chuẩn như LocalDateTime, là ổn định.

Tệp cấu hình là tệp văn bản thuần tuý có một lớp trên mỗi hàng. Nhận xét, ký tự đại diện đơn và kép được hỗ trợ. Dưới đây là ví dụ về cấu hình:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

Để bật tính năng này, hãy truyền đường dẫn của tệp cấu hình đến các tuỳ chọn trình biên dịch Compose.

Groovy

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Vì trình biên dịch Compose chạy riêng biệt trên từng mô-đun trong dự án, nên bạn có thể cung cấp cấu hình cho nhiều mô-đun nếu cần. Ngoài ra, hãy có một cấu hình ở cấp gốc của dự án và truyền đường dẫn đó đến từng mô-đun.

Nhiều mô-đun

Một vấn đề thường gặp khác liên quan đến cấu trúc nhiều mô-đun. Trình biên dịch Compose chỉ có thể suy ra xem một lớp có ổn định hay không nếu tất cả các kiểu gốc mà lớp đó tham chiếu được đánh dấu rõ ràng là ổn định hoặc trong một mô-đun cũng được tạo bằng trình biên dịch Compose.

Nếu lớp dữ liệu nằm trong một mô-đun riêng biệt với lớp giao diện người dùng (phương pháp đề xuất), thì đây có thể là vấn đề mà bạn gặp phải.

Giải pháp

Để giải quyết vấn đề này, bạn có thể làm theo một trong các phương pháp sau:

  1. Thêm các lớp này vào Tệp cấu hình Trình biên dịch.
  2. Bật trình biên dịch Compose trên các mô-đun lớp dữ liệu hoặc gắn thẻ các lớp bằng @Stable hoặc @Immutable khi thích hợp.
    • Quá trình này bao gồm việc thêm một phần phụ thuộc Compose vào lớp dữ liệu. Tuy nhiên, đây chỉ là phần phụ thuộc cho thời gian chạy Compose chứ không dành cho Compose-UI.
  3. Trong mô-đun giao diện người dùng, hãy gói các lớp lớp dữ liệu trong các lớp trình bao bọc dành riêng cho giao diện người dùng.

Vấn đề tương tự cũng xảy ra khi sử dụng các thư viện bên ngoài nếu các thư viện đó không dùng trình biên dịch Compose.

Không phải thành phần kết hợp nào cũng có thể bỏ qua

Khi tìm cách khắc phục các vấn đề về độ ổn định, bạn không nên tìm cách để có thể bỏ qua mọi thành phần kết hợp. Việc cố gắng làm như vậy có thể dẫn đến tình trạng tối ưu hoá sớm gây ra nhiều vấn đề hơn là khắc phục.

Có nhiều trường hợp quảng cáo có thể bỏ qua không mang lại lợi ích thực sự nào và có thể dẫn đến việc khó duy trì mã. Ví dụ:

  • Một thành phần kết hợp không được kết hợp lại thường xuyên hoặc hoàn toàn không.
  • Một thành phần kết hợp chỉ gọi các thành phần kết hợp có thể bỏ qua.
  • Một thành phần kết hợp có số lượng lớn tham số với các cách triển khai tương đương tốn kém. Trong trường hợp này, chi phí kiểm tra xem có tham số nào đã thay đổi hay không có thể lớn hơn chi phí kết hợp lại giá rẻ.

Khi một thành phần kết hợp có thể bỏ qua, bạn sẽ phải chi thêm một mức hao tổn nhỏ và có thể không đáng kể. Thậm chí, bạn có thể chú thích thành phần kết hợp là không thể khởi động lại trong trường hợp bạn xác định rằng việc có thể khởi động lại sẽ tốn nhiều chi phí hơn so với giá trị.