Anda mungkin akan mengalami kesalahan umum Compose. Kesalahan ini dapat memberi Anda kode yang tampaknya berjalan cukup baik, tetapi dapat menurunkan performa UI. Ikuti praktik terbaik untuk mengoptimalkan aplikasi Anda di Compose.
Menggunakan remember untuk meminimalkan penghitungan yang mahal
Fungsi composable dapat berjalan sangat sering, sesering setiap frame animasi. Oleh karena itu, sebisa mungkin Anda perlu melakukan sedikit penghitungan dalam isi composable.
Teknik penting adalah menyimpan hasil penghitungan dengan
remember. Dengan demikian, penghitungan dijalankan sekali, dan Anda dapat mengambil hasilnya kapan pun dibutuhkan.
Misalnya, berikut adalah beberapa kode yang menampilkan daftar nama yang diurutkan, tetapi melakukan pengurutan dengan cara yang sangat mahal:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { LazyColumn(modifier) { // DON’T DO THIS items(contacts.sortedWith(comparator)) { contact -> // ... } } }
Setiap kali ContactsList direkomposisi, seluruh daftar kontak akan diurutkan lagi, meskipun daftar belum berubah. Jika pengguna men-scroll daftar,
Composable akan direkomposisi setiap kali baris baru muncul.
Untuk mengatasi masalah ini, urutkan daftar di luar LazyColumn, dan simpan
daftar yang diurutkan dengan remember:
@Composable fun ContactList( contacts: List<Contact>, comparator: Comparator<Contact>, modifier: Modifier = Modifier ) { val sortedContacts = remember(contacts, comparator) { contacts.sortedWith(comparator) } LazyColumn(modifier) { items(sortedContacts) { // ... } } }
Sekarang, daftar akan diurutkan sekali, saat ContactList pertama kali dikomposisi. Jika
kontak atau pembanding berubah, daftar yang diurutkan akan dibuat ulang. Jika tidak,
composable dapat terus menggunakan daftar yang diurutkan ke cache.
Selalu teruskan input apa pun yang direferensikan di dalam blok penghitungan remember sebagai
kunci sehingga nilai yang di-cache diperbarui saat input tersebut berubah. Karena remember
menyimpan kunci dan hasil yang di-cache dalam tabel slot dan membandingkan kunci menggunakan
Object.equals() pada setiap penerusan rekomposisi, hanya gunakan remember saat meng-cache
penghitungan yang mahal atau alokasi objek yang berat (seperti pemformat tanggal,
shader, atau pengurutan daftar) atau saat identitas objek harus dipertahankan.
Berhati-hatilah saat meneruskan koleksi besar atau objek dengan implementasi
equals() yang mahal sebagai kunci ke remember untuk penghitungan yang murah, karena
membandingkan kunci dapat memerlukan biaya lebih besar daripada menjalankan penghitungan secara langsung:
@Composable
fun ContactHeader(contacts: List<Contact>) {
// Avoid: Comparing the `contacts` list with .equals() on recomposition
// is more expensive than reading contacts.size or contacts.firstOrNull() directly.
val firstContactName = remember(contacts) {
contacts.firstOrNull()?.name.orEmpty()
}
Text(firstContactName)
}
Menggunakan kunci tata letak lambat dan jenis konten
Tata letak lambat menggunakan kembali item secara efisien, hanya membuat ulang atau merekomposisi item jika diperlukan. Namun, Anda dapat membantu mengoptimalkan tata letak lambat untuk rekomposisi dan penggunaan kembali slot.
Misalkan operasi pengguna menyebabkan item dipindahkan dalam daftar. Sebagai contoh, Anda menampilkan daftar catatan yang diurutkan menurut waktu modifikasi, dengan catatan yang terakhir diubah di bagian atas.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes ) { note -> NoteRow(note) } } }
Namun, ada masalah dengan kode ini. Misalnya, catatan bawah diubah. Sekarang catatan tersebut menjadi yang paling baru diubah, sehingga masuk ke bagian atas daftar, dan setiap catatan lainnya berpindah satu slot ke bawah.
Tanpa bantuan Anda, Compose tidak menyadari bahwa item yang tidak diubah hanya dipindahkan di dalam daftar. Sebagai gantinya, Compose menganggap "item 2" yang lama telah dihapus dan yang baru telah dibuat untuk item 3, item 4, dan seterusnya. Hasilnya adalah Compose merekomposisi setiap item dalam daftar, meskipun sebenarnya hanya satu item yang berubah.
Solusinya di sini adalah dengan menyediakan kunci item. Memberikan kunci yang stabil untuk setiap item memungkinkan Compose terhindar dari rekomposisi yang tidak perlu. Dalam hal ini, Compose dapat menentukan bahwa item yang sekarang berada di slot 3 adalah item yang sama dengan yang sebelumnya ada di slot 2. Karena tidak ada data untuk item tersebut yang berubah, Compose tidak perlu merekomposisi item.
@Composable fun NotesList(notes: List<Note>) { LazyColumn { items( items = notes, key = { note -> // Return a stable, unique key for the note note.id } ) { note -> NoteRow(note) } } }
Saat menggunakan tata letak lambat (LazyColumn, LazyRow, LazyGrid), perhatikan juga
panduan berikut:
- Gunakan kunci unik yang dapat disimpan di Bundle: Kunci harus unik, deterministik,
dan dapat disimpan di
BundleAndroid karena tata letak lambat menggunakanrememberSaveablesecara internal. Pilih jenis primitif sepertiInt,Long, atauString, dan jangan pernah menggunakan indeks daftar atauhashCode()sebagai kunci. - Selalu tentukan
contentTypeuntuk daftar heterogen: TeruskancontentType = { it.type }keitemsatauitemsIndexedagar Compose hanya menggunakan kembali slot komposisi di antara item yang memiliki struktur visual yang sama selama men-scroll. - Gunakan
itemsIndexed, bukanindexOf: Hindari memanggillist.indexOf(item)di dalam blokitems, yang mengubah tata letak menjadi pemindaianO(N²); gunakanitemsIndexedsebagai gantinya. - Angkat status bersama dan pembacaan
CompositionLocal: Kumpulkan instanceFlowbersama dan baca nilaiCompositionLocal(sepertiLocalContext.currentatauLocalDensity.current) sekali dalam composable induk di atas tata letak lambat dan teruskan nilai yang di-resolve ke composable item. - Gunakan
RowatauColumnuntuk daftar kecil yang dibatasi: Untuk koleksi statis atau terbatas yang kecil (seperti 1 hingga 10 item),RowatauColumnstandar denganforEachmenghindari overhead virtualisasi lambat dan pengambilan data di awal.
Menggunakan computedStateOf atau derivedStateOf untuk membatasi rekomposisi
Salah satu risiko menggunakan status dalam komposisi adalah, jika status cepat berubah, UI Anda mungkin akan direkomposisi lebih banyak dari yang diperlukan. Misalnya, saat Anda menampilkan daftar yang dapat di-scroll. Anda memeriksa status daftar untuk melihat item mana yang merupakan item pertama yang terlihat di daftar:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
Masalah yang ada di sini adalah, jika pengguna men-scroll daftar, listState akan terus
berubah saat pengguna menyeret jarinya. Itu berarti daftar terus-menerus
direkomposisi. Namun, sebenarnya Anda tidak perlu merekomposisi sesering itu—Anda
tidak perlu merekomposisi hingga item baru terlihat di bagian atas. Jadi,
cara itu membuat banyak komputasi tambahan, yang membuat UI Anda berperforma buruk.
Solusinya adalah menggunakan status turunan (atau computedStateOf di
Compose Runtime 1.13 dan yang lebih tinggi). Status turunan memungkinkan Anda memberi tahu Compose tentang
perubahan status yang sebenarnya harus memicu rekomposisi. Dalam hal ini, tentukan
bahwa Anda mementingkan kapan item yang pertama kali terlihat berubah. Saat nilai status tersebut
berubah, UI harus merekomposisi, tetapi jika pengguna belum men-scroll cukup banyak
untuk membawa item baru ke atas, UI tidak perlu merekomposisi.
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton by remember { derivedStateOf { listState.firstVisibleItemIndex > 0 } } AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
Pilih pendekatan turunan status yang sesuai berdasarkan versi Compose dan biaya penghitungan Anda:
remember { computedStateOf { ... } }(Compose Runtime 1.13 dan yang lebih baru): Mengevaluasi ulang penghitungannya pada setiap pembacaan tanpa meng-cache hasilnya, dan hanya membatalkan komposisi saat output yang dihitung berubah. Karena menghindari overhead pembukuan cache dan write-back invalidasi, pilihremember { computedStateOf { listState.firstVisibleItemIndex > 0 } }untuk pemeriksaan kondisi dasar dan turunan ringan yang didorong oleh status yang berubah dengan cepat (seperti offset scroll).remember { derivedStateOf { ... } }: Menyimpan hasil penghitungannya ke dalam cache di beberapa pembacaan. GunakanderivedStateOfpada versi Compose yang lebih rendah dari 1.13, atau saat penghitungan itu sendiri memerlukan komputasi yang mahal dan dibaca beberapa kali selama penerusan komposisi. Jangan teruskan nilai status snapshot yang dibaca di dalamderivedStateOf(sepertilistState.firstVisibleItemIndex) sebagai kunciremember, karena tindakan ini akan membuat ulangDerivedStatepada setiap perubahan status dan mencegahnya dilewati.- Pengambil Kotlin biasa (
get()) dan pembacaan properti langsung: Pilih pengambil kustom biasa untuk ekspresi boolean sederhana yang tidak memerlukan peredaman invalidasi, dan baca koleksi.sizesecara langsung, bukan menggabungkannya dalamderivedStateOf. Hindari penggunaanproduceStatedengansnapshotFlowuntuk turunan status sinkron, karena akan menambah overhead pengiriman coroutine yang tidak perlu.
Menunda pembacaan selama mungkin
Saat masalah performa teridentifikasi, menunda pembacaan status dapat membantu. Menunda pembacaan status akan memastikan bahwa Compose menjalankan kembali kode minimum yang mungkin pada rekomposisi. Misalnya, jika UI Anda memiliki status yang diangkat di hierarki composable bagian atas dan Anda membaca status dalam composable turunan, Anda dapat menggabungkan status yang dibaca dalam fungsi lambda. Tindakan ini akan membuat proses baca hanya terjadi jika benar-benar diperlukan. Sebagai referensi, lihat implementasi di aplikasi contoh Jetsnack. Jetsnack menerapkan efek seperti toolbar yang diciutkan di layar detailnya. Untuk memahami alasan teknik ini berfungsi, lihat postingan blog Jetpack Compose: Men-debug Rekomposisi.
Untuk mencapai efek ini, composable Title memerlukan offset scroll
untuk mengimbanginya sendiri menggunakan Modifier. Berikut adalah versi sederhana kode
Jetsnack sebelum pengoptimalan dilakukan:
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack, scroll.value) // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scroll: Int) { // ... val offset = with(LocalDensity.current) { scroll.toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
Saat status scroll berubah, Compose membatalkan validasi cakupan rekomposisi induk
terdekat. Dalam hal ini, cakupan terdekat adalah composable
SnackDetail. Perhatikan bahwa Box adalah fungsi inline, sehingga bukan cakupan rekomposisi. Jadi, Compose merekomposisi SnackDetail dan composable apa pun di dalam
SnackDetail. Jika Anda mengubah kode agar hanya membaca status tempat Anda benar-benar
menggunakannya, Anda dapat mengurangi jumlah elemen yang perlu direkomposisi.
@Composable fun SnackDetail() { // ... Box(Modifier.fillMaxSize()) { // Recomposition Scope Start val scroll = rememberScrollState(0) // ... Title(snack) { scroll.value } // ... } // Recomposition Scope End } @Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... val offset = with(LocalDensity.current) { scrollProvider().toDp() } Column( modifier = Modifier .offset(y = offset) ) { // ... } }
Parameter scroll kini menjadi lambda. Itu berarti Title masih dapat mereferensikan
status yang diangkat, tetapi nilainya hanya dibaca di dalam Title, yang benar-benar
diperlukan. Akibatnya, saat nilai scroll berubah, cakupan rekomposisi
terdekat sekarang menjadi composable Title–Compose tidak perlu lagi merekomposisi
seluruh Box.
Ini adalah peningkatan yang bagus, tetapi Anda dapat melakukan yang lebih baik lagi! Anda harus curiga
jika Anda menyebabkan rekomposisi hanya menata ulang atau menggambar ulang Composable. Dalam
hal ini, yang Anda lakukan hanyalah mengubah offset composable Title,
yang dapat dilakukan dalam fase tata letak.
@Composable private fun Title(snack: Snack, scrollProvider: () -> Int) { // ... Column( modifier = Modifier .offset { IntOffset(x = 0, y = scrollProvider()) } ) { // ... } }
Sebelumnya, kode ini menggunakan Modifier.offset(x: Dp, y: Dp), yang menganggap
offset sebagai parameter. Dengan beralih ke versi lambda pengubah,
Anda dapat memastikan fungsi membaca status scroll dalam fase tata letak. Oleh karena
itu, saat status scroll berubah, Compose dapat melewati fase
komposisi sepenuhnya, dan langsung menuju fase tata letak. Saat meneruskan variabel Status yang sering
diubah menjadi pengubah, Anda harus menggunakan versi lambda
pengubah jika memungkinkan.
Berikut adalah contoh lain dari pendekatan ini. Kode ini belum dioptimalkan:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
Di sini warna latar belakang kotak beralih dengan cepat di antara dua warna. Dengan demikian, status ini sering berubah. Composable ini akan membaca status ini di pengubah latar belakang. Akibatnya, kotak tersebut harus merekomposisi di setiap frame, karena warna berubah di setiap frame.
Untuk meningkatkannya, gunakan pengubah berbasis lambda—dalam hal ini, drawBehind.
Artinya status warna hanya dibaca selama fase menggambar. Sehingga,
Compose dapat melewati fase komposisi dan tata letak sepenuhnya—saat warna
berubah, Compose akan langsung ke fase menggambar.
val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .drawBehind { drawRect(color) } )
Menghindari penulisan mundur
Compose memiliki asumsi inti bahwa Anda tidak akan pernah menulis ke status yang telah dibaca. Saat melakukannya, hal ini disebut penulisan mundur dan dapat menyebabkan rekomposisi terjadi di setiap frame, tanpa henti.
Composable berikut menampilkan contoh jenis kesalahan ini.
@Composable fun BadComposable() { var count by remember { mutableIntStateOf(0) } // Causes recomposition on click Button(onClick = { count++ }, Modifier.wrapContentSize()) { Text("Recompose") } Text("$count") count++ // Backwards write, writing to state after it has been read</b> }
Kode ini memperbarui jumlah di akhir composable, setelah membacanya pada baris sebelumnya. Jika menjalankan kode ini, Anda akan melihat bahwa setelah mengklik tombol, yang menyebabkan rekomposisi, penghitung dengan cepat terus meningkat tanpa henti saat Compose merekomposisi Composable ini, melihat pembacaan status yang sudah tidak berlaku, serta menjadwalkan rekomposisi lain.
Anda dapat menghindari penulisan mundur seluruhnya dengan tidak pernah menulis ke status di
Komposisi. Jika memungkinkan, selalu tulis status sebagai respons terhadap peristiwa
dan lambda seperti dalam contoh onClick sebelumnya.
Untuk panduan mendalam tentang penulisan mundur, loop rekomposisi, dan koordinasi fase, lihat Penulisan mundur di Compose.
Referensi lainnya
- Panduan performa aplikasi: Temukan praktik terbaik, library, dan alat untuk meningkatkan performa di Android.
- Periksa Performa: Memeriksa performa aplikasi.
- Tolok ukur: Menjalankan tolok ukur performa aplikasi.
- Startup aplikasi: Optimalkan startup aplikasi.
- Profil dasar pengukuran: Memahami profil dasar pengukuran.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Status dan Jetpack Compose
- Pengubah Grafik
- Paradigma Compose