Ada beberapa kesalahan umum Compose yang mungkin Anda temui. Kesalahan ini dapat memberi Anda kode yang tampaknya berjalan cukup baik, tetapi dapat menurunkan performa UI. Bagian ini mencantumkan beberapa praktik terbaik untuk membantu Anda menghindarinya.
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, sortComparator) {
contacts.sortedWith(sortComparator)
}
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.
Menggunakan kunci tata letak lambat
Tata letak lambat melakukan upaya terbaik untuk menggunakan kembali item dengan cerdas, hanya membuat ulang atau merekomposisi item jika diperlukan. Namun, Anda dapat membantunya membuat keputusan terbaik.
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. Begitu juga 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 memahami 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)
}
}
}
Menggunakan 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 bawah. Jadi,
cara itu membuat banyak komputasi tambahan, yang membuat UI Anda berperforma buruk.
Solusinya adalah menggunakan status turunan. 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()
}
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. Anda dapat melihat cara kami menerapkan pendekatan ini pada aplikasi contoh Jetsnack. Jetsnack menerapkan efek seperti toolbar yang diciutkan di layar detailnya. Untuk memahami alasan teknik ini berfungsi, lihat postingan blog: Men-debug Rekomposisi.
Untuk mencapai efek ini, composable Title
perlu mengetahui
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 mencari cakupan rekomposisi induk
terdekat dan membatalkan validasinya. Dalam hal ini, cakupan terdekat adalah
composable SnackDetail
. Catatan: Box adalah fungsi inline, sehingga tidak bertindak
sebagai cakupan rekomposisi.
Jadi, Compose merekomposisi SnackDetail
,
dan juga merekomposisi 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, kita dapat menggunakan 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 { mutableStateOf(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 }
Kode ini memperbarui jumlah di akhir composable, setelah membacanya pada baris di atas. 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.