Alat & Performa

Tingkatkan performa Anda di Compose

Speaker

Tentang

Tentang konferensi

Jetpack Compose memudahkan desain antarmuka pengguna berkualitas tinggi, tetapi mungkin sulit untuk melacak masalah yang memengaruhi performa UI. Jenis masalah ini dapat menyebabkan masalah yang terlihat oleh pengguna, seperti latensi startup, dan frame yang dilewati yang muncul sebagai jank scroll atau animasi yang tersendat-sendat.

Dalam diskusi ini, kami akan memperkenalkan alat untuk mengungkap hal yang memengaruhi performa UI Anda, dengan berfokus pada Perfetto dan pelacakan (re-)komposisi. Kemudian, kami akan membahas anti-pola tertentu yang mungkin Anda gunakan dan diam-diam merugikan Anda, cara menemukannya dalam kode, dan pola positif yang dapat Anda gunakan untuk menggantikannya. Setelah menyelesaikan pembahasan ini, Anda akan memahami cara berpikir dalam Compose dengan pola pikir performa, serta cara membuat profil, menganalisis, dan memverifikasi pengoptimalan performa di aplikasi Anda.

Ringkasan

  • Rekaman pelacakan Perfetto mencatat aktivitas perangkat di perangkat dan menampilkan slice Compose seperti Komposisi dan tata letak di sepanjang thread utama. Gunakan untuk memeriksa linimasa frame dan melacak durasi fase.
  • Setiap operasi di dalam composable memiliki biaya, termasuk meluncurkan dan membatalkan coroutine. Audit API efek samping dan pertimbangkan untuk menggabungkan efek dengan kunci yang sama untuk mengurangi overhead komposisi.
  • Pelacakan komposisi menganotasi fungsi composable, dan pelacakan rekomposisi (1.13+) melacak pembacaan dan penulisan status yang menyebabkan rekomposisi. Fitur ini membantu mendiagnosis pekerjaan yang tidak terduga langsung dalam rekaman aktivitas Perfetto.
  • ComputedState (Compose 1.13+) menghindari invalidasi yang tidak diinginkan dan biasanya lebih cepat daripada DerivedState selama invalidasi baca-setelah-tulis. DerivedState tetap berguna saat penghitungan atau alokasi yang mahal mendapatkan manfaat dari caching.
  • Melewati composable memerlukan biaya pemeliharaan dan overhead perbandingan parameter. Andalkan kemampuan lewati yang kuat dan kesetaraan identitas objek, atau hindari invalidasi sepenuhnya, daripada memaksakan stabilitas.

Keahlian

Resource

Gemini Notebook

Topik

  • jetpack-compose
  • android-performance
  • perfetto
  • pembuatan tolok ukur
  • coroutine
  • pelacakan-komposisi
  • android-cli

Transkrip

Pengantar

  • Halo.

  • Halo semuanya.

  • Semoga istirahat Anda cukup.

  • Semoga Anda sudah minum kopi, memakai pelembap, dan sebagainya.

  • Kita akan membahasnya lebih mendalam hari ini.

  • Ya, ini dia.

  • Pembahasan yang sedikit lebih mendalam hari ini.

  • Nama saya Andrei Shikov, dan hari ini saya berbagi panggung dengan Andrew Bailey.

  • Kami berdua bekerja di tim runtime compiler Compose, tetapi juga banyak hal lain, termasuk performa.

  • Tema diskusi hari ini adalah menjawab pertanyaan Anda, pertanyaan tentang performa, karena Jetpack Compose ditetapkan sebagai toolkit UI default untuk pengembangan Android.

  • Dan, sekarang banyak orang bertanya bagaimana kita dapat memastikan bahwa Compose UI kita memiliki performa sebaik mungkin.

  • Jadi, hari ini kita akan membahasnya secara mendalam, seperti yang sudah saya katakan.

  • Selain itu, kami juga akan memperkenalkan beberapa alat untuk membantu Anda meningkatkan performa di Compose.

  • Pertama-tama, saya ingin membahas mengapa performa penting bagi kami.

  • Saat memikirkan kualitas dan membangun aplikasi berkualitas, Anda biasanya memikirkan animasi, kami memikirkan set fitur yang kaya, dan terkadang performa terabaikan, tetapi persepsi pengguna sangat bergantung pada performa aplikasi Anda.

  • Jadi, jika animasi tersendat, persepsi akan langsung menurun.

  • Jadi, semua metrik, angka, yang akan kita sebutkan hari ini, sebenarnya adalah metrik proxy yang kita pilih untuk mewakili perasaan pengguna tentang aplikasi, perasaan mereka tentang semua animasi tersebut, semua hal yang terjadi di layar.

  • Jadi, pada dasarnya, mengoptimalkan aplikasi Anda berarti mengoptimalkan pengalaman yang lebih baik.

  • Jadi, ini hanyalah pola pikir yang ingin kami tanamkan kepada Anda, seiring kita mendalami banyak topik hari ini.

  • Sekarang, saat Anda mulai melihat performa aplikasi, saya ingin Anda melakukan tiga hal cepat, tiga hal yang sangat cepat.

  • Pertama, jika Anda mengukur performa, Anda mengukurnya dalam mode rilis, sehingga kita tidak memiliki banyak informasi yang dapat di-debug dan semua ini memperlambat aplikasi kita.

  • Kedua, Anda mengaktifkan R8 atau penyingkat lainnya.

  • Tahun ini, kami memperkenalkan pengoptimalan yang meningkatkan performa coroutine dua kali lipat, dan kami juga melakukan banyak hal lainnya.

  • Kami sangat mengandalkan R8 saat mengoptimalkan Compose.

  • Dan yang terakhir, terutama untuk metrik startup, Anda harus memastikan bahwa Anda menggunakan profil dasar pengukuran.

  • Jadi, class yang Anda muat saat startup telah dikompilasi dan dimuat sebelumnya saat aplikasi tersebut dimulai.

  • Jadi, seperti yang Anda lihat, ini seperti perbedaan 6x dari build aplikasi yang dapat di-debug seperti biasa.

  • Dan, saat Anda mengukur semua hal tersebut, hal itu akan menambah keberhasilan lain yang Anda lakukan di dalam kode.

  • Sekarang, Anda telah melakukan semua itu, aplikasi masih sedikit lambat, atau masih ada masalah lain terkait performa, dan di sinilah Anda ingin masuk ke loop performa ini.

  • Anda ingin mulai melihat beberapa metrik pada beberapa skenario tertentu.

  • Misalnya, aplikasi Anda memerlukan waktu terlalu lama untuk dimuat, jadi mungkin Anda ingin mereset aplikasi, memulainya dari peluncur seperti biasa, dan memeriksa berapa lama waktu yang diperlukan aplikasi untuk menampilkan layar data pertama.

  • Atau mungkin aplikasi sedikit tersendat, jadi Anda men-scroll-nya, dan memastikan bahwa scroll ini dapat diulang dan stabil.

  • Jadi, lingkungan yang stabil di slide ini sangat penting, karena saat melakukan pengoptimalan nanti, Anda ingin memastikan bahwa Anda melihat peningkatan pada metrik, dan Anda dapat mengonfirmasi bahwa pengoptimalan tersebut benar-benar berfungsi sebagaimana mestinya.

  • Benchmark sangat cocok untuk hal ini, karena benchmark sudah menangkap banyak data, melakukan banyak iterasi untuk memastikan tidak ada banyak gangguan di perangkat Anda, dan juga melakukan banyak penyiapan perangkat yang dapat membantu Anda.

  • Selain itu, tolok ukur juga merupakan skenario yang dapat diulang secara default, dan inilah yang kami upayakan di sini.

  • Namun, Anda juga dapat melakukannya dengan banyak skrip jika mau.

  • Dan yang terakhir, Anda membuat profil, meningkatkan sesuatu di aplikasi, melihat data, lalu mengulangi lagi, dan mungkin mengulangi lagi dengan metrik skenario yang berbeda saat aplikasi Anda mengalami kesulitan.

Membuat profil dengan rekaman aktivitas Perfetto

  • Saat Anda melihat data, alat pertama yang kami perkenalkan hari ini dan yang harus Anda pahami dengan baik adalah rekaman aktivitas Perfetto.

  • Rekaman aktivitas Perfetto merekam semua yang terjadi di perangkat saat Anda merekam aktivitas tersebut, misalnya, selama menjalankan macrobenchmark, atau mungkin beberapa skrip yang Anda rekam.

  • Hal ini sangat membantu setiap engineer yang menangani performa Android.

  • Saya menghabiskan sebagian besar waktu kerja saya untuk melihat rekaman aktivitas Perfetto akhir-akhir ini.

  • Tampilannya akan terlihat seperti ini.

  • Data ini sangat banyak.

  • Seperti yang saya katakan, rekaman aktivitas menangkap semua yang terjadi di perangkat.

  • Jadi, Anda mungkin kesulitan pada awalnya untuk mengetahui di mana tepatnya aplikasi Anda berada, jadi sebaiknya gunakan kotak penelusuran yang praktis ini.

  • Telusuri Compose, telusuri paket aplikasi Anda, dan hal-hal semacam itu.

  • Fokuskan diri Anda pada hal-hal yang sudah dikenal.

  • Saat Anda memfokuskan diri dan menemukan proses aplikasi, Anda akan melihat garis waktu render frame dan durasi berbagai hal di aplikasi Anda.

  • Jika kita mengisolasi salah satunya, Anda akan melihat sesuatu seperti ini.

  • Yang pertama adalah pekerjaan thread utama.

  • Di sinilah Compose melakukan sebagian besar tugasnya.

  • Lalu ada RenderThread.

  • Di sinilah kita memproses perintah kanvas, mengenkodekannya ke GPU.

  • Kemudian, GPU melakukan tugasnya, dan sistem menggabungkannya.

  • Kita hanya akan melihat thread utama hari ini.

  • Karena di sinilah Compose melakukan tugasnya.

  • Kita sedang membahas Compose, jadi wajar jika kita hanya berfokus pada hal itu.

  • Dan saat kita mengisolasi satu frame tersebut di thread utama, kita akan mulai melihat slice Compose sekarang.

  • Saya memilih beberapa di antaranya untuk Anda.

  • Prosesnya seperti komposisi, pengukuran, tata letak, gambar.

  • Jika Anda pernah mendengar tentang fase Compose, Anda akan melihatnya saat melihat performa sebelumnya.

  • Jadi, fase-fase ini memiliki slice masing-masing dalam rekaman aktivitas Perfetto, dan sangat mudah untuk mengetahui durasi setiap fase, serta fase mana yang berjalan di setiap frame tertentu.

  • Jadi, kita telah membahas banyak hal dan panjang lebar tentang cara mengoptimalkan beberapa pekerjaan ini, dan Anda ingin memastikan bahwa Anda melakukan pekerjaan yang tepat pada fase yang tepat.

  • Misalnya, jika Anda memiliki animasi yang memperbarui warna kotak atau semacamnya, Anda hanya ingin membatalkan fase gambar, bukan komposisi, tata letak, dan hal-hal semacam itu.

  • Ini adalah dasar-dasar performa, dan hari ini kita akan membahasnya lebih dalam, atau kita akan membahas beberapa hal lebih dalam, dan kita akan berfokus pada komposisi, khususnya.

  • Sekarang, mari kita dengarkan penjelasan Andrew tentang hal itu.

  • Terima kasih, Andrei.

  • Seperti yang disebutkan Andrei, sebelumnya kita menghabiskan banyak waktu untuk membahas konteks performa tentang pengoptimalan seperti menghindari pembacaan mundur, memastikan Anda menunda pembacaan status ke fase yang tepat dalam siklus proses komposisi, dan mengoptimalkan, serta memastikan composable Anda tidak valid seperti yang Anda harapkan, sehingga Anda tidak melakukan terlalu banyak pekerjaan pada setiap frame.

  • Ini adalah titik awal yang sangat baik, tetapi kami sudah memiliki banyak materi tentang hal ini, dan kami ingin memajukan diskusi tersebut sedikit lebih jauh.

  • Jadi, hari ini kita akan membahas apa yang tersisa dalam komposisi setelah Anda melakukan pengoptimalan tersebut, dan membahas apa yang dilakukan composable Anda.

  • Kenyataan yang tidak menyenangkan dari semua hal di dalam kode Compose, dan semua hal yang Anda kirimkan dengan aplikasi terkait performa, adalah semuanya memiliki biaya yang terkait.

  • Tidak ada kode yang Anda kirimkan di aplikasi yang gratis, dan bahkan hal-hal sepele seperti membaca status, dan menyusun hal-hal seperti efek samping di dalam komposisi, memiliki beberapa biaya nyata yang terkait dengannya, dan penting untuk memperhatikan hal-hal tersebut, terutama saat Anda mulai membedah apa yang sebenarnya dilakukan composable, dan mulai meningkatkan performa di dalam aplikasi itu sendiri.

  • Untuk menunjukkan seperti apa sebenarnya dalam skala paling mikroskopis, berikut beberapa operasi terkecil yang dapat Anda lakukan di mana saja dalam kode, dan terutama di dalam composable, dan pada resolusi ini, kita berbicara tentang skala nanodetik dalam hal seberapa besar biaya yang diperlukan untuk hal-hal ini sebagai pola pikir.

  • Jika ingin meningkatkan performa, Anda memiliki dua opsi.

  • Anda dapat menghapus operasi murah dalam jumlah yang sangat besar, atau operasi yang lebih mahal dalam jumlah yang lebih kecil.

  • Khususnya pada diagram ini, coroutine berada di sisi yang lebih mahal dalam hal operasi ini, dan ini menjadi contoh yang baik untuk memulai pencarian, karena jika kita menghapus beberapa instance ini, kita dapat mulai melihat manfaat yang lebih nyata dalam aplikasi kita dengan lebih cepat.

  • Hal lain yang perlu diingat tentang coroutine adalah, seperti yang saya tunjukkan di slide, tidak hanya peluncuran coroutine yang memerlukan biaya, tetapi juga pembatalan coroutine.

  • Biaya pembatalan sangat penting karena hal ini terjadi terutama saat Anda menghapus konten dan mungkin berpindah ke area lain di dalam aplikasi.

  • Menutup halaman adalah kasus yang sangat mahal di banyak aplikasi, yang mengharuskan Anda memanfaatkan waktu render frame sebaik mungkin, dan memastikan bahwa Anda menggunakan sebagian besar dari apa yang Anda miliki.

  • Jadi, penting untuk memperhatikan kedua biaya ini.

  • Semua ini, apa pun yang kita lihat, akan bertambah pada skala sekitar 16 juta nanodetik untuk digunakan.

  • Jadi, kita memiliki beberapa anggaran di sini yang perlu kita kurangi, dan coroutine akan menjadi contoh yang baik untuk memulai.

Mengelola efek samping

  • Di Compose, coroutine paling sering muncul dengan API efek.

  • Ada berbagai API di sini yang dapat Anda gunakan dengan SideEffects, mulai dari fungsionalitas tinggi hingga latensi rendah.

  • Di sisi paling fungsional dari spektrum ini, kita memiliki LaunchedEffect, yang mencakup semua hal untuk pekerjaan asinkron, terutama seperti animasi, apa pun yang memerlukan cakupan coroutine.

  • Namun, jika Anda tidak membutuhkannya, dan Anda dapat menyelesaikan tugas dengan API yang lebih murah, sebaiknya pertimbangkan untuk menggunakannya jika sesuai dengan kebutuhan kasus penggunaan Anda.

  • Untuk menunjukkan seperti apa penerapannya, di sini, saya memiliki composable pemutar media untuk menunjukkan semua SideEffect yang berbeda dan kapan SideEffect tersebut mungkin muncul.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Asynchronous work and animations
        LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
    }
  • Kita dapat memulai dengan LaunchedEffect.

  • Dalam contoh ini, digunakan untuk memuat data secara asinkron di dalam pemutar media.

  • Jika Anda memerlukan cakupan coroutine, atau jika Anda memerlukan coroutine secara umum atau untuk melakukan panggilan penangguhan, gunakan LaunchedEffect.

  • Meskipun masih lebih mahal dan ada biaya yang terkait dengan coroutine, selama Anda memanfaatkannya dan membenarkannya, gunakan API yang tepat untuk tugas tersebut.

  • Namun, jika Anda berada dalam situasi saat Anda menggunakan LaunchedEffect, tetapi tidak memanfaatkan coroutine, sebaiknya pertimbangkan API lain yang dapat melakukan hal yang sama, tetapi dengan biaya yang lebih rendah.

  • Untuk itu, dengan melihat spektrum yang lebih rendah, kita dapat melihat contoh efek samping lain seperti DisposableEffect.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val activity = LocalActivity.current
    
        // Setup and teardown
        DisposableEffect(Unit) {
            activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
            onDispose {
                activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
            }
        }
    }
  • DisposableEffect sangat bagus jika Anda memiliki proses penyiapan dan penghapusan yang terkait dengan efek Anda.

  • Di sini, dalam contoh pemutar media yang sama, saya memiliki contoh yang mengunci orientasi aktivitas ke lanskap saat composable ada, lalu melepaskan kunci orientasi tersebut saat composable hilang beberapa saat kemudian.

  • Jika Anda menginginkan efek satu kali dari LaunchedEffect dan DisposableEffect, tetapi tidak memerlukan coroutine, dan tidak memiliki efek dispose, Anda juga dapat menggunakan SideEffect baru dengan API kunci yang kami perkenalkan di Compose 1.12.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // One-shot events
        SideEffect(Unit) { mediaPlayer.play() }
    }
  • Ini adalah alternatif yang bagus untuk dua API lainnya jika Anda telah menggunakannya, baik LaunchedEffect karena kemudahan, atau DisposableEffect karena kebutuhan akan efek satu kali, menavigasi atau bermigrasi ke API SideEffect dengan kunci dapat mencapai hal yang sama dengan overhead yang lebih rendah dan performa yang lebih baik, terutama jika dimigrasikan beberapa kali selama rentang seluruh aplikasi.

  • Terakhir, API yang paling murah adalah SideEffect API standar.

    @Composable
    fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
        val mediaPlayer = rememberMediaPlayer()
    
        // Synchronize the outside world to the state of composition
        SideEffect { mediaPlayer.playbackRate = playbackRate }
    }
  • Hal ini paling sesuai jika Anda ingin menyinkronkan status luar dunia dengan apa yang terjadi dalam komposisi.

  • Jadi di sini, sekali lagi dalam contoh pemutar media, saya memiliki contoh lain yang menunjukkan sinkronisasi beberapa status pemutaran dengan beberapa parameter yang diterima oleh komposisi.

  • Satu hal yang perlu diingat, jika Anda memutuskan untuk mengaudit penggunaan efek samping dan bermigrasi ke penggunaan yang lebih murah, perlu diingat bahwa selain perilaku dan kasus penggunaan lain yang telah kita bahas, ada juga perbedaan dalam jaminan pengurutan dengan efek samping.

  • Semua efek sekali pakai dijalankan terlebih dahulu, diikuti dengan efek samping reguler, baik yang diberi kunci maupun tidak, dan setelah frame selesai, semua efek yang diluncurkan akan berjalan karena ada pengiriman yang terlibat.

  • Jika Anda bergantung pada pengaturan waktu ini untuk efek yang diluncurkan, ingatlah hal tersebut jika Anda mempertimbangkan untuk bermigrasi.

  • Untuk migrasi apa pun, pastikan untuk menguji kode Anda dan memastikan bahwa Anda tidak bergantung pada jaminan waktu ini dan kode Anda masih berfungsi setelahnya.

  • Jika tidak dapat mengurangi kompleksitas efek samping itu sendiri, Anda juga masih dapat mengurangi jumlah efek samping yang dimiliki dengan menggabungkannya.

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) { viewModel.loadCart() }
        LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
        LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }
    
        CartContent(...)
    }
  • Hal ini sangat penting untuk dipertimbangkan jika Anda memiliki beberapa efek yang menggunakan kunci yang sama.

  • Ini adalah kandidat yang baik untuk dipertimbangkan untuk digabungkan karena dapat dijalankan secara bersamaan.

  • Di sini, dalam contoh ini, saya memiliki beberapa efek yang diluncurkan, yang masing-masing melakukan panggilan asinkron ke beberapa model tampilan.

  • Jika saya menggantinya dengan satu LaunchedEffect, lalu panggilan peluncuran lainnya di dalamnya, meskipun saya masih memiliki jumlah coroutine yang sama yang berjalan di sini, overhead-nya tetap lebih sedikit karena tugas dikeluarkan dari komposisi dan dimasukkan ke coroutine.

    @Composable
    fun ShoppingCartScreen(
        viewModel: CartViewModel
    ) {
        LaunchedEffect(Unit) {
            launch { viewModel.loadCart() }
            launch { viewModel.loadPaymentMethods() }
            launch { viewModel.loadDeliveryAddresses() }
        }
    
        CartContent(...)
    }
  • Jadi, sebaiknya pertimbangkan untuk memigrasikan efek di tempat dalam situasi seperti ini untuk mengurangi jumlah efek jika memungkinkan.

  • Efek dan coroutine hanyalah salah satu contoh tempat yang mungkin Anda audit di dalam aplikasi, tetapi setiap composable dan setiap aplikasi akan berbeda dan memiliki peluang yang berbeda di dalamnya.

  • Untuk memahami apa yang relevan untuk aplikasi Anda, kita perlu membahas alat berikutnya yang akan diperkenalkan Andrei, yaitu pelacakan komposisi.

Pelacakan komposisi dan rekomposisi

  • Oke.

  • Jadi, pelacakan komposisi, seperti yang Anda lihat pada rekreasi frame Perfetto yang kita lihat sebelumnya di slide yang berbeda, akan memberikan informasi pengaturan waktu dengan fase yang berbeda, tetapi tidak memberikan banyak detail tentang apa yang terjadi.

  • Apa?

  • Ya, apa yang terjadi di dalam komposisi tempat fungsi composable dijalankan.

  • Dan pelacakan komposisi membantu kita melakukannya.

  • Jadi, kami memperkenalkan beberapa penanda di compiler Compose yang pada dasarnya menganotasi setiap fungsi dan membuatnya terlihat di rekaman aktivitas Perfetto, sehingga Anda dapat memeriksa durasi setiap fungsi.

  • Anda dapat menggunakannya untuk mengetahui fungsi mana yang sedang direkomposisi.

  • Namun, ada sedikit overhead, jadi jika ingin mengambil data performa yang paling akurat, Anda harus menggunakan Perfetto reguler atau seperti menjalankan tanpa pengambilan Perfetto dan menggunakan metrik lain.

  • Namun, pelacakan komposisi sangat berguna jika Anda ingin mengetahui durasi setiap composable.

  • Terkadang, Anda juga ingin mengetahui alasan terjadinya rekomposisi, dan itulah yang kami coba selesaikan dalam rilis berikutnya dari pelacakan runtime Compose Tracing, yaitu 1.13.

  • Kami memperkenalkan pelacakan rekomposisi sekarang, yang menganotasi setiap pembacaan status dan penulisan status yang menghasilkan rekomposisi.

  • Jadi, Anda akan mendapatkan sesuatu seperti ini di rekaman aktivitas Perfetto, tempat Anda akan melihat setiap penulisan status dan stack panggilan tempat penulisan dan pembacaan terjadi, sehingga Anda dapat mencocokkannya dengan baris sebenarnya dalam kode tempat status tersebut diakses atau diubah.

  • Jika Anda ingin mempelajari lebih lanjut, lihat link di sini.

  • Dokumentasi ini berisi semua informasi tentang pelacakan komposisi dan pelacakan rekomposisi.

  • Sekarang, mari kita beralih ke Andrew untuk membahas cara menggunakan ini secara lebih efisien untuk menghindari penulisan status.

Pengoptimalan dan pembatalan status

  • Terima kasih, Andrei.

  • Berdasarkan hasil yang Anda temukan dari penyelidikan ini, Anda mungkin menemukan bahwa ada invalidasi dalam komposisi yang tidak berguna untuk aplikasi Anda dan seharusnya dapat dihindari sejak awal.

  • Misalnya, contoh paling umum yang saya miliki adalah beberapa tombol scroll ke atas yang bergantung pada visibilitas atau progres scroll untuk visibilitasnya.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton = scrollState.value > 0
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • Di sini, karena saya membaca nilai status scroll di dalam komposisi, ini adalah status yang sangat sering berubah, bisa berubah sebanyak sekali per frame, dan ini akan menyebabkan banyak rekomposisi yang dapat dihindari karena kondisi ini, meskipun status scroll itu sendiri berubah, kondisi sebenarnya yang kita perhatikan di sini tidak terlalu sering berubah.

  • Ini adalah contoh situasi saat kami merekomendasikan penggunaan sesuatu seperti status komputasi, yang dapat menghindari pembatalan untuk perubahan status yang tidak penting bagi aplikasi Anda.

  • Jadi, dalam contoh ini, yang saya lakukan di sini adalah menggabungkan penghitungan itu sendiri di dalam status komputasi, dan saat kita membaca status ini, alih-alih membatalkan validasi berdasarkan posisi scroll itu sendiri, status ini akan membatalkan validasi composable ini secara bersyarat berdasarkan seluruh kondisi dan memperolehnya dari status lain itu sendiri.

    @Composable
    fun ScrollToTopButton(
        scrollState: ScrollState
    ) {
        val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
        val coroutineScope = rememberCoroutineScope()
    
        if (showScrollToTopButton) {
            FloatingActionButton(
                onClick = {
                    coroutineScope.launch { scrollState.animateScrollTo(0) }
                }
            ) {
                Icon(Icons.Default.ArrowUpward, "Scroll to Top")
            }
        }
    }
  • Jika Anda pernah melihat pola ini sebelumnya, kemungkinan Anda pernah melihatnya digunakan dengan status turunan.

  • ComputedState adalah fitur baru di Compose 1.13 dan meskipun ComputedState dan DerivedState memiliki tujuan yang sama, yaitu membatalkan secara bersyarat saat penghitungannya berubah, keduanya memiliki perbedaan yang sangat kecil dalam cara melakukannya.

  • DerivedState, yang telah kami sediakan sejak awal Compose, meng-cache nilai yang dihitungnya dan menggunakannya kembali hingga dependensinya berubah.

  • Dengan ComputedState, nilai akan dihitung setiap kali Anda membacanya.

  • Caching terdengar berguna, tetapi sebenarnya dalam banyak situasi, caching lebih mahal daripada sekadar membaca hasilnya.

  • Untuk memahami cara kerjanya, kita dapat melihat contoh penggunaan status ini di dalam aplikasi Anda dan mempelajari fungsi masing-masing status.

  • Di sini, saya memiliki status yang sama yang kurang lebih mencerminkan apa yang kita lakukan di tombol scroll ke atas, yang memiliki beberapa kondisi berdasarkan beberapa status lain, dan saya memilikinya sebagai DerivedState dan ComputedState.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    // First read
    derivedState.value
    computedState.value
    
    // Second read
    derivedState.value
    computedState.value
  • Saya juga membaca status dua kali dan kita akan mendapatkan nilai yang sama pada kedua pembacaan, tetapi proses di balik layar berbeda di antara keduanya.

  • Untuk menyederhanakan pelacakan hal yang sebenarnya akan muncul di sini dalam sesuatu seperti Perfetto, kita dapat memeriksa hal yang sebenarnya terjadi saat Anda membaca kedua status ini.

  • Di sini, kita mulai dengan memanggil computedState.value.

  • Lambda penghitungan akan dieksekusi, yang kemudian membaca nilai status, dan ada juga sedikit pemeliharaan untuk memberi tahu bahwa pembacaan telah terjadi.

  • Hal ini akan berlaku untuk semua status snapshot di Compose.

  • Pembacaan kedua berperilaku sama persis dengan pembacaan pertama dan memiliki karakteristik performa yang sama secara keseluruhan.

  • Sebagai perbandingan, pembacaan pertama DerivedState melakukan hal yang serupa, yaitu masih harus memanggil lambda penghitungan dan masih melakukan notifikasi lainnya, tetapi memiliki sedikit overhead pengelolaan cache, yang sedikit diabaikan pada pertama kalinya karena tidak ada cache yang perlu diperiksa.

  • Jadi, library ini melakukan sedikit pemeliharaan untuk caching itu sendiri, lalu menyelesaikan hal yang sama dalam waktu yang sedikit lebih lama.

  • Namun, pembacaan kedua untuk DerivedState sebenarnya lebih cepat daripada pembacaan kedua untuk ComputedState, meskipun keduanya merupakan status yang cukup sederhana.

  • Seperti yang saya katakan di slide sebelumnya, biaya pembacaan snapshot tidaklah sepele dalam beberapa kasus, dan pembacaan dari cache sebenarnya lebih cepat daripada melakukan pembacaan itu sendiri, itulah sebabnya kita melihat kemenangan yang sangat tipis untuk pembacaan kedua itu sendiri untuk DerivedState.

  • Namun, skenario menarik yang sebenarnya adalah apa yang terjadi saat status perlu dibatalkan.

  • Inilah yang kami sebut skenario baca setelah tulis, saat Anda memperbarui salah satu status yang digunakan dalam penghitungan dan penghitungan harus dijalankan lagi.

  • Dalam kode, kita melakukan sesuatu seperti mengubah a, mungkin menambahkannya, lalu membaca nilainya lagi atau mungkin merekomposisi.

    var a by mutableStateOf(0)
    
    val derivedState = derivedStateOf { a > 0 }
    val computedState = computedStateOf { a > 0 }
    
    ...
    
    // Read-after-write
    a++
    derivedState.value
    computedState.value
  • Tampilan dalam diagram sebenarnya akan sama persis untuk ComputedState.

  • Tidak ada cache yang perlu dikelola, ini terlihat sama persis, pertama, kedua, ketiga, apa pun pembacaan ini sebenarnya.

  • Namun, DerivedState memiliki tugas yang sulit.

  • Sistem ini melalui jalur yang sangat mahal untuk melihat bahwa cache tidak valid bahkan sebelum memperbarui dan memanggil penghitungan itu sendiri, lalu harus melakukan pelacakan dependensi dan mengelola banyak pengamatan untuk menulis kembali nilai cache dan memahami bahwa semua nilai ini benar-benar berubah dan mengelola pemeliharaan itu sendiri.

  • Semua ini bertambah dan membuat biaya pembatalan DerivedState sangat mahal dibandingkan dengan ComputedState.

  • Untuk membuktikan semua klaim ini, ComputedState biasanya lebih cepat pada pembacaan pertama sekitar 200 ns.

  • ComputedState kalah sekitar 350 ns di Pixel 6, khususnya, dalam kondisi tolok ukur tertentu.

  • Jadi, pada pembacaan kedua, ComputedState akan sedikit kalah dibandingkan dengan DerivedState.

  • Namun, dalam kasus yang benar-benar kita perhatikan, yaitu saat kita membatalkan validasi dan mungkin saat Anda benar-benar memiliki pekerjaan yang harus dilakukan pada frame, ComputedState hampir dua kali lebih cepat, bahkan lebih, dibandingkan dengan DerivedState.

  • Jadi, bergantung pada tujuan penggunaan status, ComputedState dapat menjadi pilihan pertama yang sangat baik.

  • Secara umum, masih ada keputusan yang harus dibuat saat menggunakan DerivedState dan ComputedState.

  • Kami tidak menghentikan penggunaan ComputedState, masih ada penggunaan untuk keduanya.

  • Umumnya, jika Anda tidak yakin mana yang harus dipilih, status komputasi adalah default yang baik.

  • Status turunan paling berguna saat Anda benar-benar memanfaatkan caching, yang berarti Anda melakukan sesuatu yang tidak biasa seperti algoritma yang rumit, beberapa alokasi besar, banyak pembacaan status di dalamnya yang benar-benar memanfaatkan caching di sini, atau apa pun yang tidak biasa atau yang bergantung pada hal-hal seperti kesetaraan objek dan kesetaraan identitas objek, tempat Anda membuat perbedaan dan mendapatkan manfaat caching yang ditawarkan status turunan.

Pengabaian dan stabilitas di Compose

  • Hal penting lain yang ingin kami bahas adalah bahwa status turunan dan status komputasi sama-sama mencoba menghindari pembatalan composable itu sendiri.

  • Hal ini berbeda dengan melewati composable, yang biasanya kita bicarakan saat memikirkan tentang pembatalan bersyarat, atau tidak melakukan pekerjaan di dalam composable.

  • Fakta yang ingin saya ungkapkan di sini adalah meskipun ada banyak pembicaraan tentang stabilitas dan pengabaian di dalam Compose, ini bukanlah segalanya, dan tidak selalu menjadi hal yang tepat untuk dioptimalkan.

  • Seperti yang telah kami sampaikan sebelumnya, semua hal dalam komposisi memiliki biaya terkait, termasuk melewati.

    // What you write
    @Composable fun ContactListItem(
        contact: Contact
    ) { ... }
    
    // What you get
    @Composable fun ContactListItem(
        contact: Contact,
        $composer: Composer?
        $changed: Int
    ) {
        val $dirty = $changed or
            $composer.changed(contact)
    
        if ($dirty) { /* Your function body */ }
        else $composer.skipToGroupEnd()
    }
  • Melewati langkah ini tidak selalu mudah.

  • Ada banyak pemeliharaan yang harus kita lakukan untuk mengaktifkannya sejak awal.

  • Kita harus melakukan hal-hal seperti menyimpan semua parameter Anda dan menemukan ruang untuk parameter tersebut pada komposisi awal, lalu memperbarui data tersebut saat Anda melakukan rekomposisi.

  • Dan kita juga harus membandingkan nilai Anda dengan nilai sebelumnya untuk memahami apakah composable Anda harus dilewati atau tidak.

  • Lalu setelah itu, semua ini hanyalah kekuatan pemeriksaan if yang melewati kode atau tidak.

  • Hal ini mungkin mahal, bergantung pada apa yang terjadi di sini.

  • Ada pengoptimalan di balik layar untuk compiler, yaitu jika Anda meneruskan parameter yang sama, kita dapat menggunakan kembali pemeriksaan kesetaraan, tetapi ini bukan satu-satunya cara, dan cukup mudah untuk mengalahkannya.

  • Untuk melihat contoh sedikit pengabaian, kita dapat melihat contoh yang sangat klasik, yaitu daftar scroll yang memiliki objek besar yang berisi semua data yang akan disusun di layar.

    data class ContactsModel(
        val header: String,
        val contacts: List<Contact>
    )
    
    data class Contact(...)
    
    @Composable
    fun ContactsScreen(model: ContactsModel) {
        Column {
            TopAppBar(title = { Text(model.header) })
            ContactsList(model.contacts)
        }
    }
    
    @Composable
    fun ContactsList(contacts: List<Contact>) {
        LazyColumn {
            items(contacts) { ContactListItem(it) }
        }
    }
    
    @Composable
    fun ContactListItem(contact: Contact) { ... }
  • Jadi di sini, model layar berisi sesuatu seperti layar header, lalu semua daftar kontak yang kemudian diteruskan melalui beberapa composable yang pada akhirnya memanggil lazy list, lalu menampilkannya di layar dengan beberapa sel item.

  • Hierarki komposisi untuk kode ini terlihat seperti ini, di mana kita memulai lagi dengan layar kontak, menyebar sedikit, lalu memiliki beberapa composable lain yang memanggil dan menampilkan item kita.

  • Jika kita melihat cara data diteruskan, kita mulai dengan model di atas.

  • Kita mendestrukturisasinya menjadi string, lalu daftar. Daftar tersebut kemudian dikonversi menjadi lambda item untuk daftar lambat, dan daftar lambat akan menyusun semua item yang muncul di layar.

  • Mari kita lihat apa yang terjadi saat kita mengubah kontak terakhir dalam daftar ini.

  • Yang kita inginkan adalah hanya membatalkan item terakhir dalam daftar, tetapi bukan itu yang akan terjadi di sini.

  • Jika kita melihat trace komposisi untuk rekomposisi ini, tampilannya akan terlihat seperti ini, di mana kita mulai dari layar kontak dan akhirnya turun ke item daftar ini.

  • Dan ini adalah cara yang cukup mahal untuk turun ke sini.

  • Kita juga dapat melihat bahwa ada semua bagian ini yang sepertinya tidak ada yang terjadi di dalam rekaman aktivitas.

  • Meskipun tidak terlihat jelas di sini, di sinilah composable sebenarnya memutuskan apakah akan dilewati atau tidak.

  • Untuk contoh ini, hanya dua composable yang dilewati karena cara daftar lambat melakukan rekomposisi.

  • Saat Anda memberinya lambda konten baru, lambda tersebut akan membatalkan semua item yang dimilikinya.

  • Jadi, Compose akan membatalkan validasi dua kontak yang tidak berubah selain satu kontak yang berubah, dan kedua kontak tersebut adalah satu-satunya item di sini yang dilewati.

  • Semua hal lain melakukan perbandingan kesetaraan yang dilewati ini hanya untuk menyadari bahwa hal itu sebenarnya harus dijalankan.

  • Jika objek Anda stabil, perbandingan ini sebenarnya bisa cukup mahal.

  • Jika Anda memaksa kami untuk memeriksa semua struktur data Anda, hal ini dapat menambah biaya yang tidak sedikit hanya untuk mengetahui apakah dua objek sama.

  • Dan yang lebih buruk, karena kita telah melakukan destrukturisasi, kita sebenarnya harus melakukan perbandingan yang sama pada daftar sebanyak dua kali.

  • Jika Anda membiarkan compiler berperilaku dengan perilaku default dan menggunakan strong skipping, Anda masih bisa mendapatkan skipping dan Anda masih bisa mendapatkan pemeriksaan untuk melewati composable secara bersyarat, tetapi composable akan menggunakan rute yang jauh lebih cepat dengan menggunakan kesetaraan identitas objek, bukan kesetaraan struktural.

  • Hal ini jauh lebih cepat untuk dihitung, dan meskipun kita merekomposisi item yang sama pada frame ini, yang sebenarnya penting adalah kita masuk ke sana dengan cepat, kita tidak membuang terlalu banyak waktu untuk mencari tahu bahwa kita sebenarnya tidak dapat melewati composable ini.

  • Inilah sebabnya kami menekankan penggunaan alat seperti benchmark dan rekaman aktivitas untuk memahami apa yang terjadi di dalam aplikasi Anda, bukan jumlah rekomposisi secara langsung.

  • Meskipun dapat menjadi proxy yang berguna, metrik ini tidak perlu terlalu diperhatikan atau diutamakan selama pengoptimalan aplikasi Anda.

  • Ingat, daripada mencoba melewati composable, hindari membatalkan validasinya sejak awal.

Menganalisis eksekusi dengan pelacakan metode

  • Dalam contoh ini, kita melihat bahwa trace komposisi dapat memberikan banyak informasi tentang apa yang terjadi, tetapi juga tidak selalu menunjukkan gambaran keseluruhan.

  • Untuk itu, Andrei akan memberi tahu kita tentang pelacakan metode.

  • Oke.

  • Seperti yang sudah dikatakan Andrew, pelacakan komposisi tidak selalu memberi tahu Anda semua yang terjadi, hanya karena pelacakan ini hanya menganotasi fungsi composable, dan pasti ada beberapa kasus saat Anda hanya melihat ruang kosong dalam pelacakan komposisi yang sudah menambah pelacakan Perfetto, tetapi masih tidak menampilkan semua yang terjadi di sana.

  • Dan terkadang, hal itu bisa berupa sesuatu yang dilakukan Compose, terkadang bisa berupa sesuatu dalam kode Anda, sangat sulit untuk mengetahuinya hanya dengan melihat trace komposisi di sini.

  • Yang ingin Anda lakukan sekarang adalah mengambil pelacakan metode.

  • Ada semacam item di profiler di Android Studio, atau Anda juga dapat menggunakan perintah ADB untuk mengambil rekaman aktivitas, dan yang akan Anda dapatkan adalah setiap panggilan fungsi yang terjadi di dunia Java.

  • Semua yang dienkode oleh Compose Compiler, semua yang dilakukan oleh Compose Runtime, dan semua yang dilakukan oleh kode Anda, dan sangat berguna untuk melacak, menyelidiki apa yang sebenarnya dieksekusi, fungsi mana yang dipanggil dalam ruang tersebut.

  • Satu hal, saya ingin-- melihat item ini di tengah.

  • Perlu diperhatikan bahwa pelacakan metode memiliki overhead yang sangat tinggi.

  • Anda memiliki overhead 10 hingga 20 kali lebih banyak, bergantung pada seberapa kompleks pohon fungsi, karena setiap panggilan fungsi menambahkan sedikit hal.

  • Untuk mengilustrasikannya, kita dapat melihat screenshot.

  • Biasanya, seperti inilah tampilan pelacakan metode untuk awal aplikasi, dan Anda mungkin bertanya, "Bagaimana cara memahaminya?"

  • Biasanya, Anda memulai pelacakan komposisi dengan melihat fungsi yang ada di sini.

  • Jadi, ini seperti pelacakan komposisi yang disederhanakan, yang memiliki ScreenContent, beberapa Button di dalamnya, dan beberapa fungsi lainnya.

  • Misalnya, kita ingin mengetahui apa yang terjadi di ScreenContent.

  • Perfetto memiliki kotak penelusuran.

  • Anda memuat pelacakan metode di penampil Perfetto, Anda menelusuri fungsi yang sama seperti yang Anda miliki di sini, ScreenContent, dan akan mendapatkan hasil seperti itu, yang merupakan pelacakan metode untuk layar.

  • Jadi, Anda dapat melihat di sini ada beberapa panggilan Composer.startGroup dan grup.

  • Inilah yang dilakukan runtime dan compiler untuk Anda di latar belakang untuk memastikan komposisi Anda benar-benar berfungsi, tetapi panggilan yang menarik di sini adalah Composer.changed.

  • Dengan cara ini, kita memeriksa apakah parameter berubah antara pemanggilan composable sebelumnya dan saat ini.

  • Dan sebagian besar Composer.changed ini didominasi oleh ScreenModel.equals ini.

  • Jadi, mungkin ScreenModel adalah semacam class data besar, dan mungkin kita membuatnya stabil untuk membantu jumlah rekomposisi, jadi mungkin ini adalah sesuatu yang muncul dalam laporan compiler.

  • Dan mungkin menghapus anotasi stabil ini akan membantu meningkatkan performa dalam kasus khusus ini.

  • Jadi, Anda harus mencari hal tersebut, dan pelacakan metode komposisi adalah alat terbaik untuk menemukannya.

  • Oke.

  • Kita telah membahas banyak hal hari ini, jadi sekarang mari kita dengarkan Andrew untuk mengakhiri sesi ini.

Alat desain dan performa composable yang cepat

  • Sebagai penutup dan ringkasan umum dari semua yang telah kita bahas hari ini, ada beberapa hal yang perlu diingat saat Anda mendesain composable, lalu mengoptimalkan aplikasi yang sudah ada.

  • Pertama-tama, ingatlah bahwa composable yang lebih murah dan composable yang tidak Anda batalkan lebih baik daripada composable yang Anda lewati.

  • Ingatlah bahwa saat Anda mendesain composable, sebaiknya pertimbangkan argumen yang lebih kecil dan lebih mudah dibandingkan, serta mungkin menggunakan kembali perbandingan terhadap panggilan fungsi lain untuk memanfaatkan pengoptimalan.

  • Saat Anda menggunakan API di dalam composable, perhatikan bahwa performa atau waktu render frame akan menjadi produk dari biaya API itu sendiri dan seberapa sering Anda menggunakannya.

  • Perhatikan bagaimana biaya ini bertambah, dan perhatikan juga bahwa mungkin ada biaya penghapusan yang terkait dengannya yang mungkin Anda alami pada waktu yang paling tidak tepat untuk aplikasi Anda.

  • Selain itu, ingatlah bahwa penting untuk menjadi warga negara yang baik saat melakukan pengoptimalan di dalam aplikasi Anda sendiri.

  • Mulailah dengan melakukan benchmark untuk membuat keputusan yang tepat, membuat profil aplikasi untuk memastikan bahwa Anda menargetkan area kode yang tepat, dan memvalidasi bahwa Anda membuat perubahan dan melihat hasil yang diharapkan dari modifikasi yang Anda buat sebagai hasil dari benchmark dan pembuatan profil.

  • Selain itu, ingatlah bahwa mungkin ada konsekuensi antara kemudahan pemeliharaan dan performa di dalam aplikasi Anda.

  • Jadi, lakukan yang terbaik untuk Anda dan tim Anda guna memastikan Anda tidak melakukan pengoptimalan berlebihan dan menyulitkan diri sendiri dalam hal pemeliharaan.

  • Selain itu, untuk mendapatkan bantuan lebih lanjut, ada satu alat lagi yang dapat Anda gunakan, yaitu agen AI yang sudah Anda miliki.

  • Kami telah meluncurkan kemampuan performa Jetpack Compose sebagai bagian dari Android CLI dan sebagai bagian dari kemampuan yang kami kirimkan di sana. Kemampuan ini memiliki banyak informasi yang telah kita bahas hari ini dan dapat memeriksa serta mengaudit aplikasi Anda dan memberikan saran untuk jenis pengoptimalan ini.

  • Anda dapat menginstalnya di agen yang ada menggunakan Android CLI.

    android skills add jetpack-compose-performance
  • Untuk menemukan informasi selengkapnya tentang Android CLI, Anda dapat mempelajarinya lebih lanjut di developer.android.com.

  • Anda juga dapat mempelajari lebih lanjut keterampilan kami di repositori GitHub kami.

  • Dengan begitu, Anda dapat melakukan tolok ukur dengan aman dan membuat kode dengan senang hati.

Kode

Cuplikan kode

Cuplikan Kotlin pada menit 09.34 di "Mengelola efek samping"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Asynchronous work and animations
    LaunchedEffect(contentSource) { mediaPlayer.load(contentSource) }
}
Cuplikan Kotlin pada menit 10:15 di "Mengelola efek samping"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val activity = LocalActivity.current

    // Setup and teardown
    DisposableEffect(Unit) {
        activity.requestedOrientation = SCREEN_ORIENTATION_LANDSCAPE
        onDispose {
            activity.requestedOrientation = SCREEN_ORIENTATION_UNSPECIFIED
        }
    }
}
Cuplikan Kotlin pada menit 10:36 di "Mengelola efek samping"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // One-shot events
    SideEffect(Unit) { mediaPlayer.play() }
}
Cuplikan Kotlin pada menit 11:12 di "Mengelola efek samping"
@Composable
fun MediaPlayer(contentSource: Uri, playbackRate: Float) {
    val mediaPlayer = rememberMediaPlayer()

    // Synchronize the outside world to the state of composition
    SideEffect { mediaPlayer.playbackRate = playbackRate }
}
Cuplikan Kotlin pada 12:12 di "Mengelola efek samping"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) { viewModel.loadCart() }
    LaunchedEffect(Unit) { viewModel.loadPaymentMethods() }
    LaunchedEffect(Unit) { viewModel.loadDeliveryAddresses() }

    CartContent(...)
}
Cuplikan Kotlin pada 12:36 di "Mengelola efek samping"
@Composable
fun ShoppingCartScreen(
    viewModel: CartViewModel
) {
    LaunchedEffect(Unit) {
        launch { viewModel.loadCart() }
        launch { viewModel.loadPaymentMethods() }
        launch { viewModel.loadDeliveryAddresses() }
    }

    CartContent(...)
}
Cuplikan Kotlin pada menit 15:12 di "Pengoptimalan dan invalidasi status"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton = scrollState.value > 0
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
Cuplikan Kotlin pada menit 15:51 di "Pengoptimalan dan invalidasi status"
@Composable
fun ScrollToTopButton(
    scrollState: ScrollState
) {
    val showScrollToTopButton by remember { computedStateOf { scrollState.value > 0 } }
    val coroutineScope = rememberCoroutineScope()

    if (showScrollToTopButton) {
        FloatingActionButton(
            onClick = {
                coroutineScope.launch { scrollState.animateScrollTo(0) }
            }
        ) {
            Icon(Icons.Default.ArrowUpward, "Scroll to Top")
        }
    }
}
Cuplikan Kotlin pada menit 16:49 di "Pengoptimalan dan invalidasi status"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

// First read
derivedState.value
computedState.value

// Second read
derivedState.value
computedState.value
Cuplikan Kotlin pada 18:35 di "Pengoptimalan dan invalidasi status"
var a by mutableStateOf(0)

val derivedState = derivedStateOf { a > 0 }
val computedState = computedStateOf { a > 0 }

...

// Read-after-write
a++
derivedState.value
computedState.value
Cuplikan Kotlin pada menit 21:07 di "Skipping dan stabilitas di Compose"
// What you write
@Composable fun ContactListItem(
    contact: Contact
) { ... }

// What you get
@Composable fun ContactListItem(
    contact: Contact,
    $composer: Composer?
    $changed: Int
) {
    val $dirty = $changed or
        $composer.changed(contact)

    if ($dirty) { /* Your function body */ }
    else $composer.skipToGroupEnd()
}
Cuplikan Kotlin pada menit 21:53 di "Skipping dan stabilitas di Compose"
data class ContactsModel(
    val header: String,
    val contacts: List<Contact>
)

data class Contact(...)

@Composable
fun ContactsScreen(model: ContactsModel) {
    Column {
        TopAppBar(title = { Text(model.header) })
        ContactsList(model.contacts)
    }
}

@Composable
fun ContactsList(contacts: List<Contact>) {
    LazyColumn {
        items(contacts) { ContactListItem(it) }
    }
}

@Composable
fun ContactListItem(contact: Contact) { ... }
Cuplikan shell pada 29:40 di "Alat desain dan performa composable yang cepat"
android skills add jetpack-compose-performance