Memperbaiki masalah stabilitas

Saat menghadapi class tidak stabil yang menyebabkan masalah performa, Anda harus mengoptimalkan cara Compose membandingkan atau membuat instance-nya. Dokumen ini menguraikan beberapa teknik yang dapat Anda gunakan untuk melakukannya.

Mengaktifkan mode skipping kuat

Mode skipping kuat diaktifkan secara default mulai Kotlin 2.0.20. Mode skipping kuat memungkinkan composable dengan parameter tidak stabil dilewati dengan membandingkan parameter tidak stabil menggunakan kesetaraan instance (===), sedangkan parameter stabil terus dibandingkan menggunakan kesetaraan struktural (Object.equals()).

Jika Anda menggunakan Kotlin versi yang lebih rendah dari 2.0.20, aktifkan mode strong skipping terlebih dahulu. Jika mode strong skipping sudah diaktifkan dan composable masih direkomposisi secara tidak perlu, periksa apakah instance baru parameter yang tidak stabil sedang dialokasikan pada setiap penerusan rekomposisi.

Lihat Strong skipping untuk mengetahui informasi selengkapnya.

Membuat class tidak dapat diubah

Anda juga dapat membuat class yang tidak stabil menjadi sepenuhnya tidak dapat diubah sehingga compiler dapat menyimpulkan stabilitasnya secara otomatis.

  • Immutable: Menunjukkan jenis yang nilai propertinya tidak dapat berubah setelah instance jenis tersebut dibuat, dan semua metode bersifat transparan secara referensial.
    • Pastikan semua properti class adalah val, bukan var, dan berjenis yang tidak dapat diubah.
    • Jenis primitif seperti String, Int, dan Float selalu immutable.
    • Jenis data class datar dengan properti primitif val atau String dalam modul yang dibuat dengan compiler Compose sudah disimpulkan sebagai stabil dan dibandingkan dengan .equals() secara otomatis—Anda tidak perlu menganotasinya dengan @Immutable.
    • Jika keabadian tidak memungkinkan, Anda harus menggunakan status Compose untuk properti yang dapat diubah.
  • Stabil: Menunjukkan jenis yang dapat diubah. Runtime Compose tidak mengetahui apakah dan kapan perilaku metode atau properti publik jenis tersebut akan memberikan hasil yang berbeda dari pemanggilan sebelumnya.

Koleksi dan stabilitas

Alasan umum mengapa Compose menganggap class tidak stabil adalah koleksi. Seperti yang disebutkan di halaman Mendiagnosis masalah stabilitas, compiler Compose tidak dapat sepenuhnya yakin bahwa antarmuka koleksi seperti List, Map, dan Set benar-benar tidak dapat diubah, sehingga menandainya sebagai tidak stabil.

Pertimbangkan class ini dari panduan Mendiagnosis masalah stabilitas:

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

Jika Mode strong skipping diaktifkan (default di Kotlin 2.0.20 dan yang lebih tinggi), class dan koleksi tidak stabil yang berisi koleksi sudah melewati rekomposisi menggunakan kesetaraan instance (===) setiap kali referensi yang sama diteruskan.

Berhati-hatilah sebelum mengganti kotlin.collections.List, Set, atau Map dengan Kotlinx Immutable Collections (ImmutableList, ImmutableSet, ImmutableMap), atau menambahkan aturan @Immutable, @Stable, atau stability_config.conf ke model penampung koleksi. Dalam mode strong skipping, List standar melewati menggunakan kesetaraan referensi (===), sedangkan ImmutableList dan model koleksi yang dianotasi menjalankan perbandingan .equals() elemen demi elemen pada setiap pass rekomposisi.

Untuk koleksi besar atau tata letak lambat (LazyColumn, LazyRow, LazyGrid), memeriksa .equals() pada setiap item dapat memerlukan waktu lebih lama daripada rekomposisi. Biaya ini menjadi dua kali lipat jika Anda meneruskan properti ImmutableList dari data class ke composable turunan:

data class SnackCategory(
    val name: String,
    val snacks: ImmutableList<Snack>
)

@Composable
fun CategorySection(category: SnackCategory) {
    Text(category.name)
    // Compose compares category.snacks twice: once in SnackCategory.equals()
    // for CategorySection, and again for HighlightedSnacks.
    HighlightedSnacks(snacks = category.snacks)
}

Daripada memaksakan kesetaraan struktural pada koleksi, pertahankan referensi instance koleksi di seluruh emisi (misalnya, dengan meng-cache transformasi di ViewModel atau dengan remember) sehingga Compose dapat melewati penggunaan kesetaraan instance (===). Pertimbangkan ImmutableList atau anotasi stabilitas pada model yang menyimpan koleksi hanya jika Anda tidak dapat mempertahankan referensi instance koleksi dan tolok ukur mengonfirmasi bahwa pemeriksaan kesetaraan struktural lebih cepat daripada rekomposisi. Jika Anda menggunakan Kotlin versi di bawah 2.0.20, aktifkan Mode Strong skipping terlebih dahulu.

Anotasi dengan Stable atau Immutable

Jalur yang memungkinkan untuk menyelesaikan masalah stabilitas untuk class kecil dengan pemeriksaan .equals() yang murah adalah dengan menganotasi class yang tidak stabil dengan @Stable atau @Immutable.

Memberi anotasi pada class akan mengganti apa yang akan disimpulkan compiler tentang class Anda. Operator ini mirip dengan !! operator di Kotlin. Anda harus sangat berhati-hati dalam menggunakan anotasi ini. Mengganti perilaku compiler dapat menyebabkan bug yang tidak terduga, seperti composable yang tidak merekomposisi saat Anda mengharapkannya.

Dengan mengaktifkan Mode lewati kuat, ikuti panduan berikut saat memutuskan apakah akan menambahkan @Stable atau @Immutable:

  • Jangan anotasi class yang sudah disimpulkan sebagai stabil: Class dengan properti val dari jenis stabil dalam modul yang dibuat dengan compiler Compose sudah disimpulkan sebagai stabil.
  • Hanya anotasi class dengan pemeriksaan .equals() yang murah saat instance dibuat ulang: Tambahkan @Stable atau @Immutable saat compiler tidak dapat menyimpulkan stabilitas (misalnya, antarmuka atau class dari modul non-Compose) dan sumber data mengalokasikan instance objek baru untuk item yang tidak berubah pada setiap emisi, asalkan class-nya kecil dan memiliki pemeriksaan .equals() yang murah.
  • Berhati-hatilah dengan model yang berisi koleksi: Hindari menambahkan @Stable atau @Immutable ke model data yang menyimpan koleksi besar (List, Set, Map), terutama untuk model yang digunakan dalam tata letak lambat (LazyColumn, LazyRow, LazyGrid, Pager). Membiarkan model tersebut tidak dianotasi memungkinkan Compose membandingkannya menggunakan kesetaraan instance (===), bukan menjalankan pemeriksaan .equals() struktural di setiap elemen.

Cuplikan berikut memberikan contoh minimal class data datar (tanpa properti koleksi) yang dianotasi sebagai tidak dapat diubah saat ditentukan dalam modul tempat compiler tidak dapat menyimpulkan stabilitas:

@Immutable
data class SnackSummary(
    val id: Long,
    val name: String,
    val price: Long
)

Baik Anda menggunakan anotasi @Immutable maupun @Stable, compiler Compose akan menandai class SnackSummary sebagai stabil dan membandingkannya menggunakan Object.equals().

Class yang dianotasi dalam koleksi

Pertimbangkan composable yang menyertakan parameter jenis List<Snack>:

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

Meskipun Snack stabil atau dianotasi dengan @Immutable, compiler Compose tetap menandai parameter snacks di HighlightedSnacks sebagai unstable karena List adalah antarmuka.

Jika Mode skipping kuat diaktifkan, HighlightedSnacks masih ditandai skippable dan melewati rekomposisi menggunakan kesetaraan instance (===) saat instance List yang sama diteruskan. Anda dapat membuat parameter snacks menjadi stable dengan menggabungkan List<Snack> dalam class wrapper @Immutable, mengganti List<Snack> dengan ImmutableList<Snack>, atau menambahkan kotlin.collections.* ke file konfigurasi stabilitas.

Namun, tindakan ini akan menyebabkan Compose menjalankan perbandingan .equals() elemen demi elemen pada setiap penerusan rekomposisi. Hanya gunakan pendekatan ini jika Anda tidak dapat mempertahankan referensi instance daftar dan benchmark menunjukkan manfaat.

File konfigurasi stabilitas

Mulai dari Compose Compiler 1.5.5, file konfigurasi class yang dianggap stabil dapat disediakan pada waktu kompilasi. Hal ini memungkinkan class yang tidak Anda kontrol, seperti class library standar seperti LocalDateTime, dianggap stabil.

File konfigurasi adalah file teks biasa dengan satu class per baris. Komentar, karakter pengganti tunggal, dan ganda didukung.

Contoh konfigurasi:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// 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<*,_>

Untuk mengaktifkan fitur ini, teruskan jalur file konfigurasi ke blok opsi composeCompiler dari konfigurasi plugin Gradle compiler Compose.

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

Karena compiler Compose berjalan di setiap modul dalam project Anda secara terpisah, Anda dapat memberikan konfigurasi yang berbeda ke modul yang berbeda jika diperlukan. Atau, miliki satu konfigurasi di level root project Anda dan teruskan jalur tersebut ke setiap modul.

Beberapa modul

Masalah umum lainnya melibatkan arsitektur multi-modul. Compiler Compose hanya dapat menyimpulkan apakah suatu class stabil jika semua jenis non-primitif yang direferensikannya ditandai secara eksplisit sebagai stabil atau dalam modul yang juga di-build dengan compiler Compose.

Jika lapisan data Anda berada dalam modul terpisah dari lapisan UI, yang merupakan pendekatan yang direkomendasikan, ini mungkin masalah yang Anda alami. Di bagian Mode lewati kuat, class dari modul non-Compose diperlakukan sebagai tidak stabil dan masih melewati rekomposisi menggunakan kesetaraan instance (===) saat instance yang sama diteruskan. Namun, jika lapisan data Anda sering mengalokasikan instance baru dari model kecil dan datar dengan nilai yang sama pada setiap emisi, Anda dapat memilih kesetaraan struktural (Object.equals()).

Solusi

Untuk mengatasi masalah ini, Anda dapat melakukan salah satu pendekatan berikut:

  1. Tambahkan class kecil dan datar dengan pemeriksaan .equals() yang murah ke File konfigurasi compiler Anda.
  2. Aktifkan compiler Compose di modul lapisan data, atau beri tag pada class dengan @Stable atau @Immutable jika sesuai.
    • Hal ini melibatkan penambahan dependensi Compose ke lapisan data Anda. Namun, ini hanyalah dependensi untuk runtime Compose, bukan untuk Compose-UI.
  3. Dalam modul UI, gabungkan class lapisan data Anda dalam class wrapper khusus UI.

Masalah yang sama juga terjadi saat menggunakan library eksternal jika tidak menggunakan compiler Compose.

Tidak semua composable harus dapat dilewati

Saat berupaya memperbaiki masalah stabilitas, Anda tidak boleh mencoba membuat setiap composable dapat dilewati atau memaksakan kesetaraan struktural pada setiap parameter. Melewati composable tidaklah gratis: setiap parameter composable yang dapat dilewati disimpan oleh runtime Compose seolah-olah diingat, dan Compose harus membandingkan setiap parameter dengan nilai sebelumnya setiap kali fungsi merekomposisi untuk menentukan apakah fungsi tersebut harus dilewati. Mencoba memaksa pengabaian di mana saja dapat menyebabkan pengoptimalan prematur yang menimbulkan lebih banyak overhead daripada penghematan.

Ada banyak situasi saat memaksakan stabilitas struktural tidak memberikan manfaat nyata dan dapat menurunkan performa atau pemeliharaan. Contoh:

  • Composable yang tidak sering atau sama sekali tidak direkomposisi.
  • Composable yang hanya memanggil composable yang dapat dilewati.
  • Composable dengan sejumlah besar parameter atau model koleksi dengan implementasi .equals() yang mahal. Dalam hal ini, biaya pemeriksaan apakah parameter telah berubah dapat lebih besar daripada biaya rekomposisi yang murah.

Jangan terlalu mengandalkan jumlah rekomposisi saja. Fokuslah untuk membuat composable yang ringan untuk dieksekusi, menunda pembacaan status yang berubah dengan cepat ke fase Tata Letak atau Gambar, dan memverifikasi perubahan dengan tolok ukur. Jika composable dapat dilewati, composable tersebut akan menambahkan sedikit overhead yang mungkin tidak sebanding. Anda bahkan dapat memberi anotasi pada composable menjadi tidak dapat dimulai ulang atau tidak dapat dilewati dalam kasus saat Anda menentukan bahwa dapat dimulai ulang atau dapat dilewati lebih banyak overhead daripada nilainya.