Mengikuti praktik terbaik

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.