Seperti kebanyakan toolkit UI lainnya, Compose merender frame melalui beberapa fase yang berbeda. Misalnya, sistem View Android memiliki tiga fase utama: pengukuran, tata letak, dan gambar. Compose sangat mirip, tetapi memiliki fase tambahan penting yang disebut komposisi di awal.
Dokumentasi Compose menjelaskan komposisi dalam Paradigma Compose serta Status dan Jetpack Compose.
Tiga fase frame
Compose memiliki tiga fase utama:
- Komposisi: UI apa yang akan ditampilkan. Compose menjalankan fungsi composable dan membuat deskripsi UI Anda.
- Tata letak: Tempat untuk menempatkan UI. Tahap ini terdiri dari dua langkah: pengukuran dan penempatan. Elemen tata letak mengukur dan menempatkan dirinya sendiri serta elemen turunan apa pun dalam koordinat 2D, untuk setiap node dalam hierarki tata letak.
- Gambar: Cara merender. Elemen UI menggambar ke dalam Canvas, biasanya layar perangkat.
Urutan tahap ini umumnya sama, memungkinkan data mengalir dalam satu
arah dari komposisi ke tata letak hingga gambar untuk menghasilkan frame (juga dikenal
sebagai aliran data searah). BoxWithConstraints, LazyColumn,
dan LazyRow adalah pengecualian penting, karena komposisi turunannya
bergantung pada fase tata letak induknya.
Secara konseptual, setiap fase ini terjadi untuk setiap frame; namun untuk mengoptimalkan performa, Compose menghindari pengulangan pekerjaan yang akan menghitung hasil yang sama dari input yang sama di semua fase ini. Compose melewati fungsi composable jika dapat menggunakan kembali hasil sebelumnya, dan Compose UI tidak akan menata ulang atau menggambar ulang seluruh hierarki jika tidak perlu. Compose hanya melakukan pekerjaan minimum yang diperlukan untuk mengupdate UI. Pengoptimalan ini dimungkinkan karena Compose melacak pembacaan status dalam berbagai fase.
Memahami fase
Bagian ini menjelaskan cara tiga fase Compose dijalankan untuk composable secara lebih mendetail.
Komposisi
Pada fase komposisi, runtime Compose menjalankan fungsi composable dan menghasilkan struktur hierarki yang mewakili UI Anda. Hierarki UI ini terdiri dari node tata letak yang berisi semua informasi yang diperlukan untuk fase berikutnya, seperti yang ditunjukkan dalam video berikut:
Gambar 2. Hierarki yang mewakili UI Anda yang dibuat dalam fase komposisi.
Subbagian kode dan hierarki UI terlihat seperti berikut:
Dalam contoh ini, setiap fungsi composable dalam kode dipetakan ke satu node tata letak dalam hierarki UI. Dalam contoh yang lebih kompleks, composable dapat berisi logika dan alur kontrol, serta menghasilkan hierarki yang berbeda dengan status yang berbeda.
Tata Letak
Pada fase tata letak, Compose menggunakan hierarki UI yang dihasilkan dalam fase komposisi sebagai input. Kumpulan node tata letak berisi semua informasi yang diperlukan untuk menentukan ukuran dan lokasi setiap node dalam ruang 2D.
Gambar 4. Pengukuran dan penempatan setiap node tata letak di hierarki UI selama fase tata letak.
Selama fase tata letak, hierarki dilalui menggunakan algoritma tiga langkah berikut:
- Mengukur turunan: Node mengukur turunannya jika ada.
- Menentukan ukuran sendiri: Berdasarkan pengukuran ini, node menentukan ukurannya sendiri.
- Menempatkan turunan: Setiap node turunan ditempatkan relatif terhadap posisi node itu sendiri.
Pada akhir fase ini, setiap node tata letak memiliki:
- Lebar dan tinggi yang ditetapkan
- Koordinat x, y tempat gambar harus digambar
Ingat hierarki UI dari bagian sebelumnya:
Untuk pohon ini, algoritma bekerja sebagai berikut:
Rowmengukur turunannya,ImagedanColumn.Imagediukur. Elemen ini tidak memiliki turunan, sehingga menentukan ukurannya sendiri dan melaporkan ukuran tersebut kembali keRow.Columndiukur berikutnya. Node ini mengukur turunannya sendiri (dua composableText) terlebih dahulu.Textpertama diukur. Elemen ini tidak memiliki turunan, sehingga menentukan ukurannya sendiri dan melaporkan ukurannya kembali keColumn.Textkedua diukur. Elemen ini tidak memiliki turunan, sehingga menentukan ukurannya sendiri dan melaporkannya kembali keColumn.
Columnmenggunakan pengukuran turunan untuk menentukan ukurannya sendiri. Tata letak ini menggunakan lebar turunan maksimum dan jumlah tinggi turunannya.Columnmenempatkan turunannya secara relatif terhadap dirinya sendiri, menempatkannya di bawah satu sama lain secara vertikal.Rowmenggunakan pengukuran turunan untuk menentukan ukurannya sendiri. Tata letak ini menggunakan tinggi turunan maksimum dan jumlah lebar turunannya. Kemudian, menempatkan turunannya.
Perhatikan bahwa setiap node hanya dikunjungi sekali. Runtime Compose hanya memerlukan satu penerusan melalui hierarki UI untuk mengukur dan menempatkan semua node, yang meningkatkan performa. Saat jumlah node dalam hierarki meningkat, waktu yang dihabiskan untuk melintasinya akan meningkat secara linear. Sebaliknya, jika setiap node dikunjungi beberapa kali, waktu traversal akan meningkat secara eksponensial.
Gambar
Pada fase menggambar, hierarki dilalui lagi dari atas ke bawah, dan setiap node menggambar dirinya sendiri di layar secara bergantian.
Gambar 5. Fase gambar menggambar piksel di layar.
Dengan menggunakan contoh sebelumnya, konten hierarki digambar dengan cara berikut:
Rowmenggambar konten apa pun yang mungkin dimilikinya, seperti warna latar belakang.Imageakan menggambar dirinya sendiri.Columnakan menggambar dirinya sendiri.Textpertama dan kedua akan menggambar dirinya sendiri.
Gambar 6. Hierarki UI dan representasi gambarnya.
Pembacaan status
Saat Anda membaca value dari snapshot state selama salah satu fase
yang tercantum sebelumnya, Compose akan otomatis melacak apa yang dilakukannya saat membaca
value. Pelacakan ini memungkinkan Compose mengeksekusi ulang pembaca saat
value status berubah, dan merupakan dasar dari kemampuan observasi status di Compose.
Anda biasanya membuat status menggunakan mutableStateOf(), lalu mengaksesnya melalui
salah satu dari dua cara berikut: dengan langsung mengakses properti value, atau
menggunakan delegasi properti Kotlin. Anda dapat membaca hal tersebut lebih lanjut di Status dalam
composable. Untuk tujuan panduan ini, "pembacaan status" mengacu pada salah satu
metode akses yang setara tersebut.
// State read without property delegate. val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(paddingState.value) )
// State read with property delegate. var padding: Dp by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.padding(padding) )
Di balik layar delegasi properti, fungsi "pengambil" dan "penyetel"
digunakan untuk mengakses dan memperbarui value Status. Fungsi pengambil dan penyetel ini hanya dipanggil saat Anda mereferensikan properti sebagai nilai, bukan saat dibuat, itulah sebabnya dua cara yang dijelaskan sebelumnya setara.
Setiap blok kode yang dapat dieksekusi ulang saat status baca berubah adalah
cakupan mulai ulang. Compose melacak perubahan value status dan memulai ulang cakupan
dalam berbagai fase.
Pembacaan status bertahap
Seperti yang disebutkan sebelumnya, ada tiga fase utama di Compose, dan Compose melacak status yang dibaca dalam setiap fase tersebut. Hal ini memungkinkan Compose hanya memberi tahu fase tertentu yang perlu melakukan pekerjaan untuk setiap elemen UI yang terpengaruh.
Bagian berikut menjelaskan setiap fase dan menjelaskan apa yang terjadi saat nilai status dibaca di dalamnya.
Fase 1: Komposisi
Pembacaan status dalam fungsi @Composable atau blok lambda memengaruhi komposisi
dan mungkin fase berikutnya. Saat value status berubah, recomposer menjadwalkan eksekusi ulang semua fungsi composable yang membaca
value status tersebut. Perhatikan bahwa runtime dapat memutuskan untuk melewati beberapa atau semua
fungsi composable jika input belum berubah. Lihat Melewati jika input
belum berubah untuk mengetahui informasi selengkapnya.
Bergantung pada hasil komposisi, Compose UI menjalankan fase menggambar dan tata letak. Android mungkin melewati fase ini jika konten tetap sama dan ukuran serta tata letak tidak akan berubah.
var padding by remember { mutableStateOf(8.dp) } Text( text = "Hello", // The `padding` state is read in the composition phase // when the modifier is constructed. // Changes in `padding` will invoke recomposition. modifier = Modifier.padding(padding) )
Fase 2: Tata Letak
Fase tata letak terdiri dari dua langkah: pengukuran dan penempatan. Langkah
pengukuran menjalankan lambda pengukuran yang diteruskan ke composable Layout,
metode MeasureScope.measure dari antarmuka LayoutModifier, dan sebagainya.
Langkah penempatan menjalankan blok penempatan fungsi layout, blok lambda
Modifier.offset { … }, dan fungsi serupa.
Pembacaan status selama setiap langkah ini memengaruhi tata letak dan kemungkinan
fase menggambar. Saat value status berubah, Compose UI akan menjadwalkan fase
tata letak. Compose UI juga menjalankan fase menggambar jika ukuran atau posisi telah berubah.
var offsetX by remember { mutableStateOf(8.dp) } Text( text = "Hello", modifier = Modifier.offset { // The `offsetX` state is read in the placement step // of the layout phase when the offset is calculated. // Changes in `offsetX` restart the layout. IntOffset(offsetX.roundToPx(), 0) } )
Fase 3: Menggambar
Pembacaan status selama menggambar kode memengaruhi fase menggambar. Contoh umumnya
meliputi Canvas(), Modifier.drawBehind, dan Modifier.drawWithContent. Saat
value status berubah, Compose UI hanya menjalankan fase menggambar.
var color by remember { mutableStateOf(Color.Red) } Canvas(modifier = modifier) { // The `color` state is read in the drawing phase // when the canvas is rendered. // Changes in `color` restart the drawing. drawRect(color) }
Mengoptimalkan pembacaan status
Karena Compose melakukan pelacakan pembacaan status yang dilokalkan, Anda dapat meminimalkan jumlah pekerjaan yang dilakukan dengan membaca setiap status dalam fase yang sesuai.
Perhatikan contoh berikut. Contoh ini memiliki Image() yang menggunakan
pengubah offset untuk mengimbangi posisi tata letak akhirnya, sehingga menghasilkan efek
paralaks saat pengguna men-scroll.
Box { val listState = rememberLazyListState() Image( // ... // Non-optimal implementation! Modifier.offset( with(LocalDensity.current) { // State read of firstVisibleItemScrollOffset in composition (listState.firstVisibleItemScrollOffset / 2).toDp() } ) ) LazyColumn(state = listState) { // ... } }
Kode ini berfungsi, tetapi menghasilkan performa yang kurang optimal. Seperti yang tertulis, kode
membaca value status firstVisibleItemScrollOffset dan meneruskannya ke
fungsi Modifier.offset(offset: Dp). Saat pengguna men-scroll,
value firstVisibleItemScrollOffset akan berubah. Seperti yang telah Anda pelajari, Compose
melacak setiap pembacaan status sehingga dapat memulai ulang (memanggil ulang) kode pembacaan,
yang dalam contoh ini adalah konten Box.
Ini adalah contoh pembacaan status dalam fase komposisi. Hal ini tidak selalu buruk, dan sebenarnya merupakan dasar rekomposisi, yang memungkinkan perubahan data memunculkan UI baru.
Poin penting: Contoh ini tidak optimal karena setiap peristiwa scroll akan mengakibatkan seluruh konten composable dievaluasi ulang, diukur, ditata, dan akhirnya digambar. Anda memicu fase Compose pada setiap scroll meskipun konten yang ditampilkan tidak berubah, hanya posisinya. Anda dapat mengoptimalkan pembacaan status untuk hanya memicu ulang fase tata letak.
Offset dengan lambda
Tersedia versi pengubah offset lain:
Modifier.offset(offset: Density.() -> IntOffset).
Versi ini mengambil parameter lambda, tempat offset yang dihasilkan ditampilkan oleh blok lambda. Perbarui kode untuk menggunakannya:
Box { val listState = rememberLazyListState() Image( // ... Modifier.offset { // State read of firstVisibleItemScrollOffset in Layout IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2) } ) LazyColumn(state = listState) { // ... } }
Mengapa performanya lebih baik? Blok lambda yang Anda berikan ke pengubah
dipanggil saat fase tata letak (khususnya, selama langkah penempatan
fase tata letak), artinya, firstVisibleItemScrollOffset status tidak lagi
dibaca selama komposisi. Karena Compose melacak saat status dibaca,
perubahan ini berarti bahwa jika value firstVisibleItemScrollOffset berubah,
Compose hanya perlu memulai ulang fase tata letak dan gambar.
Tentu saja, biasanya pembacaan status sangat diperlukan dalam fase
komposisi. Meskipun demikian, ada beberapa kasus saat Anda dapat meminimalkan jumlah
rekomposisi dengan memfilter perubahan status. Untuk mengetahui informasi selengkapnya tentang hal ini,
lihat derivedStateOf: mengonversi satu atau beberapa objek status menjadi status
lain.
Loop rekomposisi (dependensi fase siklus)
Panduan ini sebelumnya menyebutkan bahwa fase Compose selalu dipanggil dalam urutan yang sama, dan tidak ada cara untuk mundur saat berada dalam frame yang sama. Namun, hal itu tidak melarang aplikasi masuk ke loop komposisi di seluruh frame yang berbeda. Perhatikan contoh ini:
Box { var imageHeightPx by remember { mutableIntStateOf(0) } Image( painter = painterResource(R.drawable.rectangle), contentDescription = "I'm above the text", modifier = Modifier .fillMaxWidth() .onSizeChanged { size -> // Don't do this imageHeightPx = size.height } ) Text( text = "I'm below the image", modifier = Modifier.padding( top = with(LocalDensity.current) { imageHeightPx.toDp() } ) ) }
Contoh ini mengimplementasikan kolom vertikal, dengan gambar di bagian atas, lalu
teks di bawahnya. Kode ini menggunakan Modifier.onSizeChanged() untuk mendapatkan ukuran gambar yang telah diselesaikan, lalu menggunakan Modifier.padding() pada teks untuk menggesernya ke bawah.
Konversi yang tidak wajar dari Px kembali ke Dp sudah menunjukkan bahwa kode tersebut
memiliki beberapa masalah.
Masalah pada contoh ini adalah kode tidak mencapai tata letak "akhir" dalam satu frame. Kode ini mengandalkan beberapa frame yang terjadi, yang melakukan pekerjaan yang tidak perlu, dan menyebabkan UI melompat-lompat di layar untuk pengguna.
Komposisi frame pertama
Selama fase komposisi frame pertama, imageHeightPx awalnya adalah
0. Oleh karena itu, kode menyediakan teks dengan Modifier.padding(top = 0).
Fase tata letak berikutnya memanggil callback pengubah onSizeChanged,
yang memperbarui imageHeightPx ke tinggi gambar yang sebenarnya. Compose kemudian
menjadwalkan rekomposisi untuk frame berikutnya. Namun, selama fase menggambar saat ini, teks dirender dengan padding 0, karena nilai imageHeightPx yang diperbarui belum ditampilkan.
Komposisi frame kedua
Compose memulai frame kedua, yang dipicu oleh perubahan nilai
imageHeightPx. Dalam fase komposisi frame ini, status dibaca dalam blok konten Box. Teks kini dilengkapi dengan padding yang secara akurat sesuai dengan
tinggi gambar. Selama fase tata letak, imageHeightPx ditetapkan lagi; namun,
tidak ada rekomposisi lebih lanjut yang dijadwalkan karena nilainya tetap konsisten.
Contoh ini mungkin terlihat rumit, tetapi berhati-hatilah dengan pola umum ini:
Modifier.onSizeChanged(),onGloballyPositioned(), atau beberapa operasi tata letak lainnya- Perbarui beberapa status
- Gunakan status tersebut sebagai input untuk pengubah tata letak (
padding(),height(), atau yang serupa) - Berpotensi berulang
Perbaikan untuk contoh sebelumnya adalah menggunakan primitif tata letak yang tepat. Contoh
sebelumnya dapat diterapkan dengan Column(), tetapi Anda mungkin memiliki contoh
yang lebih kompleks yang memerlukan kustomisasi, dan akan memerlukan penulisan
tata letak kustom. Lihat panduan Tata letak kustom untuk mengetahui informasi selengkapnya.
Prinsip umumnya di sini adalah memiliki satu sumber tepercaya untuk beberapa elemen UI yang harus diukur dan ditempatkan terkait dengan satu sama lain. Menggunakan primitif tata letak yang tepat atau membuat tata letak kustom berarti induk minimal berfungsi sebagai sumber tepercaya yang dapat mengoordinasikan hubungan antara beberapa elemen. Memperkenalkan status dinamis akan melanggar prinsip ini.
Untuk informasi selengkapnya tentang loop rekomposisi dan cara menghindari penulisan ke status di seluruh fase, lihat Penulisan mundur di Compose.
Direkomendasikan untuk Anda
- Catatan: teks link ditampilkan saat JavaScript nonaktif
- Status dan Jetpack Compose
- Daftar dan petak
- Kotlin untuk Jetpack Compose