Memoisasi lambda
Mode strong skipping juga memungkinkan lebih banyak memoisasi lambda di dalam fungsi composable. Sekarang secara default (atau di masa depan dengan strong skipping dinonaktifkan), compiler Compose hanya akan membungkus lambda dalam fungsi composable yang hanya menangkap nilai stabil dalam fungsi remember
, selain itu lambda composable juga akan selalu diingat.
Catatan: Lambda tanpa tangkapan juga dimemoisasi, tetapi ini dilakukan oleh compiler Kotlin dan bukan oleh plugin compiler Compose dengan membuat instance statis lambda.
Dengan strong skipping diaktifkan, lambda dengan tangkapan yang tidak stabil juga akan dimemoisasi. Ini berarti semua lambda yang ditulis dalam fungsi composable sekarang secara otomatis diingat.
Secara efektif ini membungkus lambda Anda dengan panggilan remember, serta dikunci dengan tangkapan lambda secara otomatis, mis.
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
val lambda = {
use(unstableObject)
use(stableObject)
}
}
kira-kira menjadi seperti ini dengan strong skipping diaktifkan:
@Composable
fun MyComposable(unstableObject: Unstable, stableObject: Stable) {
val lambda = remember(unstableObject, stableObject) {
{
use(unstableObject)
use(stableObject)
}
}
}
Kunci mengikuti aturan perbandingan yang sama dengan fungsi composable, kunci tidak stabil dibandingkan menggunakan ekualitas instance dan kunci stabil dibandingkan menggunakan ekualitas objek.
Catatan: Ini sedikit berbeda dengan panggilan remember normal di mana semua kunci dibandingkan menggunakan ekualitas objek.
Melakukan pengoptimalan ini sangat meningkatkan jumlah composable yang akan dilewati selama rekomposisi, karena tanpa memoisasi ini, composable apa pun yang mengambil parameter lambda kemungkinan besar akan memiliki lambda baru yang dialokasikan selama rekomposisi dan karenanya tidak akan memiliki parameter yang sama dengan komposisi terakhir.
Mengapa strong skipping bersifat eksperimental?
Mengubah composable mana yang bisa dilewati merupakan perubahan perilaku yang sangat besar untuk Compose, mungkin perubahan perilaku terbesar yang pernah kami luncurkan. Kami ingin berhati-hati dalam mengaktifkannya untuk memastikan tidak ada kasus berisiko yang akan membuat upgrade ke versi Compose baru menjadi terlalu sulit. Saat ini kami telah mengaktifkannya untuk kode kami di Compose 1.7 alfa dan akan memutuskan apakah tetap mengaktifkannya sebelum 1.7 menjadi beta.
Kami kemudian akan memutuskan apakah akan mengaktifkannya secara default di compiler untuk semua orang. Perlu diperhatikan bahwa "eksperimental" mirip dengan API lainnya. Kami rasa kodenya tidak mengandung bug, kami hanya tidak yakin bahwa kodenya sudah dalam bentuk final dan mungkin saja akan berubah di masa depan.
Jika Anda mencobanya dan menemukan masalah dengan strong skipping, harap laporkan bug di goo.gle/compose-feedback.
Apakah saya masih harus menandai tipe sebagai stabil?
Kebutuhan untuk melakukan hal ini akan menurun secara drastis, tetapi masih diperlukan dalam beberapa kasus. Kasus utama untuk hal ini adalah ketika Anda menginginkan objek Anda dibandingkan dengan ekualitas objek, bukan dengan ekualitas instance.
Kami juga telah menambahkan file konfigurasi stabilitas agar kasus ini lebih mudah dikelola. Ini memungkinkan Anda menandai class apa pun sebagai stabil. Anda bisa menggunakan file konfigurasi dengan atau tanpa mengaktifkan strong skipping, keduanya merupakan fitur yang terpisah tetapi saling melengkapi yang dirancang untuk membantu mengatasi masalah ini.
File konfigurasi sangat bagus untuk class eksternal, seperti java.time.Instant
yang tidak dapat dianotasi dengan @Stable
. Anda juga bisa menandai seluruh paket sebagai stabil jika itu membantu kasus penggunaan Anda, mis. java.time.*
.
Peringatan, konfigurasi ini tidak membuat class stabil dengan sendirinya. Sebaliknya, dengan menggunakan konfigurasi ini, Anda bersedia mengikuti kontrak stabilitas dengan compiler. Salah mengonfigurasi class bisa menyebabkan rekomposisi rusak.
Apakah seluruh aplikasi saya akan rusak ketika saya mengaktifkannya?
Seharusnya tidak! Namun, mungkin Anda akan menemukan sejumlah kegagalan uji. Ini akan berdasar pada jenis pengujian yang Anda tulis di code base, tetapi jika Anda melakukan pengujian yang secara khusus menghitung jumlah komposisi dari sebuah composable, maka jumlah tersebut kemungkinan besar akan berubah, dan pengujian akan gagal sampai Anda memperbarui jumlah tersebut.
Ini diaktifkan di Compose 1.7. 0-alfa, apakah ini berarti jika saya menggunakan 1.7 maka saya menggunakan Strong Skipping?
Tidak. Strong skipping adalah flag compiler compose, sehingga perlu diaktifkan untuk setiap modul yang digunakan. Jika Anda menggunakan Compose 1.7 tetapi tidak mengaktifkan strong skipping, composable kami akan melewati aturan strong skipping dan composable Anda akan melewati seperti biasanya. Hal yang sama berlaku untuk modul, Anda bisa memilih untuk mengaktifkan strong skipping modul satu per satu.
Apakah ada contoh yang berfungsi dengan baik sebelumnya, tetapi menjadi tidak berfungsi dengan strong skipping?
Contoh composable yang sebelumnya dapat berfungsi, tetapi tidak berfungsi saat strong skipping diaktifkan, pada umumnya karena composable tersebut berfungsi akibat efek samping yang tidak disengaja. Ini kemungkinan besar akan terjadi dengan objek nested yang dapat diubah. Cuplikan kode berikut akan berfungsi dengan strong skipping dinonaktifkan, tetapi dengan strong skipping diaktifkan, composable daftar akan dilewati.
@Composable
fun MyToggle(enabled: Boolean) {}
@Composable
fun MyList(list: List<String>) {}
@Composable
fun MyScreen() {
var list by remember { mutableStateOf(mutableListOf("Foo")) }
var toggle by remember { mutableStateOf(false) }
MyToggle(toggle)
MyList(list)
Button(
onClick = {
list.add("Bar")
toggle = !toggle
}
) { Text("Toggle") }
}
Composable ini hanya berfungsi sebelumnya karena toggle
memicu rekomposisi, dan perubahan pada daftar yang dapat diubah hanya terjadi akibat efek samping. Zach Klippenstein menerbitkan postingan yang bagus tentang masalah ini jika Anda tertarik: Dua status yang dapat diubah tidak akan menjadikannya benar.
Mengintip di balik layar pengembangan Compose
Anda bisa berhenti membaca di sini jika Anda hanya ingin mempelajari tentang mode strong skipping, tetapi saya pikir sebagian orang mungkin ingin mengetahui lebih dalam tentang apa yang berubah dan mengapa. Ini adalah pemikiran yang kami lalui ketika mengembangkan fitur Compose baru.
Mode strong skipping mengatasi dua titik masalah utama dalam pengembangan Compose:
- Composable tidak dilewati saat developer berpikir bahwa mereka harus melakukannya karena input yang tidak stabil
- "Lambda tidak stabil" didiagnosis sebagai penyebab rekomposisi
Kemampuan melewati
Ketika Compose pertama kali dikembangkan, kami sebenarnya mengira bahwa orang-orang akan kesulitan menghadapi masalah yang berbeda dari yang kita lihat sekarang. Kami pikir tim akan terus bertanya "mengapa ini tidak dikomposisi ulang?!”. Ternyata yang terjadi justru sebaliknya. Kami mengambil pendekatan konservatif untuk rekomposisi, kami percaya bahwa dalam kasus ketika kami tidak tahu jika input ke composable telah berubah (karena mereka tidak stabil dan mutasinya tidak terlacak oleh runtime compose), kami tidak boleh melewatinya. Ya, mungkin performanya sedikit menurun, tetapi ini berarti aplikasi akan menunjukkan keadaan yang benar ketika ada hal lain yang menyebabkan rekomposisi (seperti yang ditunjukkan pada contoh di atas), yang merupakan faktor terpenting bagi pengguna akhir.
Setelah Compose dirilis, kami melihat hal yang berlawanan dengan hipotesis awal kami, developer berjuang untuk memahami mengapa composable dapat dikomposisi ulang. Masalah ini diperkuat oleh layout inspector yang menunjukkan jumlah rekomposisi, ini adalah alat termudah yang tersedia untuk mendiagnosis masalah performa di aplikasi Anda, dan jangan salah paham, beberapa di antaranya sangat nyata dan perlu diperbaiki, tetapi menggunakan jumlah rekomposisi sebagai alat untuk pengoptimalan performa memiliki kelemahan yang sangat besar. Rekomposisi hanyalah salah satu bagian dari performa aplikasi Anda dan jumlah rekomposisi hanyalah ukuran tidak langsung dari kontribusi rekomposisi terhadap hal itu. Anda bisa memiliki satu composable yang sangat mahal yang menghabiskan banyak waktu untuk dikomposisi ulang, dan Anda bisa memiliki composable yang sangat murah untuk dikomposisi ulang yang memerlukan rekomposisi tambahan di sana-sini yang sebenarnya tidak sepadan karena membuat kode Anda menjadi lebih rumit dihindari. Di sini, jika Anda hanya menggunakan layout inspector, Anda mungkin menghabiskan banyak waktu untuk mengoptimalkan composable yang murah, dan sama sekali tidak menyadari bahwa composable yang hanya dikomposisi ulang sekali, sebenarnya adalah yang mahal. Inilah mengapa kami selalu mengatakan, khawatirkan tentang rekomposisi hanya ketika Anda memiliki masalah performa yang terukur. Anda harus menggunakan alat seperti pelacakan komposisi dan macrobenchmark untuk mengukur dan menguantifikasi waktu atau tenggat waktu bingkai yang terlewat jika perubahan kode Anda benar-benar membuat perbedaan pada performa akhir aplikasi Anda.
Namun demikian, kami setuju bahwa situasi saat ini dapat diperbaiki. Developer terjebak dalam situasi yang berkinerja buruk ketika menulis kode Kotlin/Compose idiomatis. Idealnya, Anda tidak perlu memikirkan semua ini, kecuali pada kasus-kasus berisiko, jadi kami coba mencari cara untuk mengatasinya.
Permintaan fitur yang sering kami lihat adalah mengapa kami tidak menampilkan stabilitas di Android Studio? Itu akan membuat proses debug jauh lebih mudah daripada menggali laporan compiler. Kami sebenarnya sudah mengimplementasikan prototipe awal fitur ini.
Kami memutuskan untuk tidak meluncurkan fitur ini karena ada dua masalah utama dengan solusi ini:
- Ini memberikan keunggulan besar pada fitur compiler yang kami tujukan sebagai kasus berisiko lanjutan. Ini akan berdampak pada developer yang berpikir bahwa mereka harus membuat setiap parameter stabil.
- Ini tidak memperbaiki masalah dengan "lambda yang tidak stabil" karena lambda tersebut tetap akan terlihat stabil. (Dibahas secara mendetail di bawah).
Kami telah menambahkan informasi ini ke debugger. Android Studio Hedgehog dan yang lebih baru akan menampilkan status rekomposisi composable ketika Anda menempatkan titik henti sementara di dalamnya, termasuk informasi tentang stabilitas.
Namun, secara umum, mengapa Anda harus tahu tentang hal ini? Dapatkah kita mencapai tujuan awal kita, yaitu stabilitas yang hanya diperlukan pada kasus berisiko? Dari sinilah kami sampai pada gagasan awal mengenai mode strong skipping, yang memungkinkan composable dengan parameter yang tidak stabil untuk melewatinya juga. Ini menggeser pendekatan default kami untuk rekomposisi dari yang semula konservatif, tidak akan melewati saat kami tidak seharusnya mendekatinya, menjadi pendekatan yang lebih seimbang, yang menurut kami lebih sesuai dengan apa yang secara intuitif Anda harapkan. Yang terpenting, kami tidak mengurangi pengalaman developer yang belum belajar tentang konsep stabilitas, kode mereka kemungkinan besar akan menjadi lebih cepat setelah kami mengaktifkannya secara default.
“Lambda yang tidak stabil”
Kita masih menghadapi masalah lambda. Mungkin salah satu kesalahpahaman terbesar tentang Compose adalah konsep "lambda yang tidak stabil" yang menyebabkan rekomposisi. Semua lambda dalam Compose bersifat stabil, jadi konsep lambda yang tidak stabil adalah konsep yang agak keliru. Jika ingin memperbaiki masalah kemampuan melewati, kita juga harus mengatasi titik masalahnya. Untuk memahaminya, kita harus mundur selangkah dan melihat compiler lebih mendalam.
Compiler Compose menentukan jika sebuah composable bisa dilewati pada waktu kompilasi dengan melihat stabilitas parameter composable. Ada beberapa composable yang tidak mungkin diketahui hingga runtime karena beberapa hal seperti tipe umum, tetapi sebagian besar composable bisa ditentukan dapat dilewati atau tidak pada waktu kompilasi. Mari kita lihat contoh composable yang menyertakan lambda:
@Composable
fun NumberComposable(
current: Long,
onValueChanged: (Long) -> Unit
) { }
Composable ini akan diproses oleh compiler dan ditandai sebagai dapat dilewati, termasuk lambda yang ditandai sebagai stabil. Laporan compiler compose akan menunjukkan hal berikut:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun NumberComposable(
stable current: Long
stable onValueChanged: Function1<Long, Unit>
)
Dalam contoh ini, bayangkan composable kemudian gunakan sebagai berikut:
@Composable
fun MyScreen(viewModel: MyViewModel) {
val number by viewModel.number.collectAsState()
var text by remember { mutableStateOf("") }
NumberComposable(
current = number,
onValueChange = { viewModel.numberChanged(it) }
)
TextField(text, onValueChanged = { text = it })
}
Developer composable ini berharap bahwa ketika pengguna mengetik di kolom teks, NumberComposable
akan melewati rekomposisi karena inputnya tidak berubah dan laporan compiler juga mengonfirmasi bahwa ia dapat dilewati. Tetapi saat runtime, mereka melihat layout inspector yang menunjukkan bahwa hal itu tidak terjadi. Contoh ini sudah cukup jelas, pada kenyataannya kami melihat developer mengalami masalah ini dan melakukan rekomposisi seluruh layar mereka dan menyebabkan jank pada perangkat berspesifikasi rendah saat pengguna mengetik, dan kami setuju bahwa ini sangat buruk!
Namun, apa yang sebenarnya terjadi di sini? Apakah laporan compiler berbohong, atau layout inspector yang salah? Tidak, perilaku ini muncul karena MyViewModel
tidak stabil dan ini menyebabkan sedikit perbedaan yang akan saya jelaskan di bawah.
Yang perlu diperhatikan adalah bahwa lambda hanyalah objek di balik layar. Ketika composable MyScreen
dikomposisi ulang, lambda onValueChanged
direalokasi. Ketika NumberComposable
dievaluasi untuk dilewati, runtime compose melihat setiap argumen yang diberikan ke dalam composable dan membandingkannya dengan nilai sebelumnya. Runtime melihat bahwa current
adalah nilai yang sama tetapi onValueChanged
telah berubah karena sudah direalokasi dan lambda hanya menggunakan ekualitas referensi untuk pengecekan ekualitasnya (alamat objek tidak sama), oleh karena itu composable akan dikomposisi ulang karena inputnya berubah. Rekomposisi disebabkan oleh objek lambda yang berubah, bukan lambda yang tidak stabil.
Jadi, mengapa tidak semua composable dengan lambda tidak pernah dilewati? Titik masalah ini adalah studi kasus mengapa kami selalu ragu-ragu menambahkan “keajaiban compiler”. Semakin banyak yang dilakukan compiler untuk Anda, semakin sulit memahami mengapa kode Anda tidak berfungsi sebagaimana mestinya. Dalam kasus ini, dahulu ketika kami mengembangkan skipping, kami menyadari bahwa ia tidak akan terlalu efektif jika semua composable dengan lambda tidak bisa melewati, itu akan menghasilkan sejumlah besar composable yang tidak pernah melewati! Jadi, kami mengimplementasikan pengingat otomatis lambda, selama mereka hanya memiliki tangkapan stabil. Jika composable di atas ditulis tanpa menggunakan viewModel
yang tidak stabil pada lambda, composable akan berperilaku seperti yang diharapkan developer. Sesuatu seperti ini
@Composable
fun NumberComposable(
current = number,
onValueChange = { stableViewModel.numberChanged(it) }
) { }
Compiler akan mentransformasi kode menjadi seperti ini:
@Composable
fun NumberComposable(
current = number,
onValueChange = remember(stableViewModel) { { stableViewModel.numberChanged(it) } }
{ }
Karena lambda tidak lagi direalokasi pada rekomposisi, berkat panggilan remember
, input ke NumberComposable
akan sama sehingga composable akan dilewati.
Mode strong skipping juga memperluas pengingat otomatis ini ke lambda dengan tangkapan tidak stabil, yang berarti setiap lambda dalam fungsi composable sekarang dimemoisasi. Ini adalah kompensasi dalam memori untuk performa runtime yang lebih baik, sesuai harapan kami. Namun kami tidak dapat memastikannya, inilah alasan lain mengapa kami memutuskan untuk meluncurkannya secara perlahan. Sejauh ini terlihat bagus, kode kami di AndroidX tidak menunjukkan adanya regresi performa saat strong skipping diaktifkan dan area-area yang belum disesuaikan stabilitasnya secara manual menunjukkan peningkatan performa yang cukup baik saat rekomposisi, dengan salah satu contohnya adalah waktu yang dibutuhkan untuk merekomposisi RadioGroup
menjadi separuhnya.
Melewati hingga akhir
Kesimpulannya, aktifkan mode strong skipping untuk mendapatkan perilaku berikut:
- Composable dengan parameter yang tidak stabil bisa dilewati.
- Parameter yang tidak stabil dibandingkan ekualitasnya melalui ekualitas instance (
===
) - Semua lambda dalam fungsi composable secara otomatis diingat. Ini berarti Anda tidak perlu lagi membungkus lambda dengan remember untuk memastikan composable yang menggunakan lambda, dilewati.
Semoga postingan ini memberikan Anda beberapa insight tentang bagaimana kami mengembangkan perubahan perilaku seperti ini. Kami ingin kode yang Anda tulis secara natural memiliki performa yang baik, tanpa Anda harus menjadi ahli di internal Compose. Namun, seperti yang sudah dijelaskan sebelumnya, kami harus berhati-hati dengan perubahan seperti ini dan bergerak secara perlahan.
Anda bisa mencoba strong skipping sekarang juga dengan flag compiler, atau menunggu kami mengaktifkannya secara default. Jika Anda mencobanya dan menemukan masalah dengan strong skipping, harap laporkan bug di goo.gle/compose-feedback.