Compose mengeksekusi frame dalam tiga fase yang berurutan secara ketat dan mengalir ke depan:
- Komposisi: Menjalankan fungsi
@Composableuntuk mem-build dan memperbarui hierarki UI. - Tata Letak: Mengukur turunan, lalu menempatkannya.
- Menggambar: Memancarkan perintah gambar kanvas untuk merender piksel ke layar.
Setiap kali State Compose dibaca selama fase apa pun, Compose akan otomatis mencatat dependensi antara status tersebut dan fase yang sesuai.
Apa yang membuat penulisan "terbalik"?
Penulisan mundur terjadi setiap kali status diubah pada fase berikutnya (atau dalam cakupan hilir—yang berarti cakupan composable dieksekusi nanti secara berurutan dalam proses komposisi yang sama) daripada tempat status dibaca, sehingga memaksa Compose untuk merekomposisi dengan menjadwalkan fase atau composable sebelumnya untuk dijalankan lagi. Penulisan mundur adalah loop rekomposisi yang tidak dioptimalkan.
Konsekuensi penulisan mundur
Penulisan mundur tidak selalu merupakan hal yang buruk, dan tidak selalu memicu error, tetapi tidak efisien dan dapat merusak performa aplikasi dalam beberapa hal:
- Rendering frame ekstra dan frame yang dilepas: Penulisan mundur memaksa Compose untuk mengeksekusi penerusan komposisi yang berlebihan di seluruh frame berurutan, membuang-buang resource CPU dan GPU serta berpotensi menyebabkan jank.
- Masalah kebenaran frame pertama: Jika komponen Anda memerlukan penulisan mundur untuk menyelesaikan dimensi atau status akhirnya, frame pertama dirender dengan data yang tidak valid, default, atau belum diselesaikan (seperti ukuran nol atau offset yang salah). Hal ini menyebabkan munculnya visual yang terlihat atau tata letak berkedip saat frame kedua dirender.
- Loop rekomposisi tak terbatas: Jika perubahan status mengubah ukuran tata letak, dan ukuran tata letak terus menulis kembali nilai baru ke status, Anda berisiko membuat loop frame tak terbatas yang menyebabkan layar terus-menerus merekomposisi setiap frame tanpa pernah stabil.
Alur maju melalui fase
Perubahan status harus selalu mengalir maju melalui fase:
| Fase Baca | Konteks Penulisan | Dapat diterima? | Mengapa |
|---|---|---|---|
Tata letak (Modifier.offset { }) |
Komposisi | Ya | Komposisi memperbarui status → Tata letak membacanya nanti dalam frame yang sama tanpa mengomposisi ulang. |
Menggambar (graphicsLayer { }, drawBehind { }) |
Komposisi | Ya | Komposisi memperbarui status → Draw membacanya pada fase akhir. Komposisi dan Tata Letak dilewati sepenuhnya. |
| Menggambar | Tata letak | Ya | Tata letak memperbarui status, dan gambar membacanya, alur yang valid. |
| Komposisi | Callback Acara (onClick, onValueChange) yang mendorong perubahan status. Catatan: callback tata letak tidak dihitung sebagai peristiwa. |
Ya | Peristiwa mengubah status yang digunakan untuk mendorong komposisi. Jika peristiwa terjadi di luar frame (tidak dalam Komposisi, Tata Letak, atau Gambar), hal ini valid. |
| Komposisi | Coroutine (LaunchedEffect) |
Ya - dengan hati-hati | Memperbarui status secara asinkron sebagai respons terhadap siklus proses/peristiwa. Penulisan dari efek dapat valid, tetapi dapat mengarah ke pelapisan status yang tidak efisien. Sebaiknya dihindari jika memungkinkan. |
| Penempatan (Dalam Tata Letak) | Ukur (Dalam Tata Letak) | Ya | Di Tata Letak, memperbarui status lalu membaca status tersebut dalam penempatan dapat diterima. |
| Ukur (Dalam Tata Letak) | Penempatan (Dalam Tata Letak) | Tidak - penulisan mundur | Menulis ke status dalam penempatan yang kemudian berada dalam status baca, menyebabkan loop pengukuran ulang. |
| Komposisi | Tata letak (onSizeChanged, LayoutModifier) |
Tidak - Penulisan mundur | Tata letak membatalkan Komposisi → Loop rekomposisi. |
| Komposisi | Menggambar (drawWithContent, Canvas) |
Tidak - Penulisan mundur | Gambar membatalkan validasi Komposisi → Loop rekomposisi. |
Kombinasi fase: Mundur versus maju
Berikut adalah contoh penulisan mundur dalam Compose dan cara menyelesaikannya.
Mundur: Membaca di Komposisi, menulis di Tata Letak
- Yang terjadi: Komposisi membaca
componentHeightuntuk menentukan UI yang akan ditampilkan. Selanjutnya dalam frame, fase Tata Letak mengukur atau menempatkan tampilan dan menulis nilai baru kecomponentHeight(misalnya, menggunakanonSizeChanged,onGloballyPositioned, atauLayoutModifierkustom). - Hasil: Mengubah
componentHeightdalam Tata Letak membatalkan fase Komposisi yang baru saja diselesaikan. Perhatikan bahwaonSizeChangedmelaporkan ukuran setelah pengukuran tata letak selesai. Jika nilai status yang diperbarui stabil pada proses berikutnya, rekomposisi dapat berhenti setelah satu frame ekstra; namun, jika nilai baru terus mengubah ukuran, hal ini akan menyebabkan loop frame tanpa batas. Selain itu,onGloballyPositionedberjalan setelah tata letak dan penempatan, sehingga penulisan status di dalamnya menjadi lebih rentan terhadap rekomposisi berkelanjutan dan loop tata ulang di seluruh frame berurutan.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
Mundur: Membaca di Komposisi, menulis di Menggambar
- Yang terjadi: Status dibaca di isi Composable (fase Komposisi),
tetapi diubah di dalam
Modifier.drawWithContent,Modifier.drawBehind, atauCanvas(fase Menggambar). - Hasil: Fase gambar memutasi status → Komposisi dibatalkan → loop tanpa akhir.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
Mundur: Membaca dalam Komposisi, menulis dalam Komposisi (fase yang sama)
- Yang terjadi: Membaca
countdalam fungsi composable, dan mengubahcountsecara langsung di slot konten composable lain setelah membacanya. - Hasil: Sistem snapshot mencatat pembacaan dan penulisan berikutnya dalam tahap komposisi yang sama, yang langsung membuat cakupan saat ini tidak valid.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
Aturan utama untuk mencegah penulisan mundur
- Jangan menulis ke status di
onGloballyPositioned,onSizeChanged, atauLayoutModifierjika status tersebut dibaca di Komposisi karena hal ini menyebabkan masalah kebenaran frame pertama.- Jika koordinat atau ukuran tata letak hanya diperlukan untuk gambar kustom, baca
langsung di tahap Gambar atau Tata Letak (misalnya, menggunakan
Modifier.drawWithCacheatauModifier.layout). - Untuk Pengukuran Tingkat Jendela (
WindowWidthSizeClass): Mengangkat pengamatan ukuran ke tingkat jendela. Cabang komposisi pada class ukuran jendela sebelum pengukuran lokal terjadi. - Menjaga Keseragaman Komposisi: Gunakan Tata Letak kustom tunggal atau komponen seperti
FlowRowatauLazyVerticalGridyang menyesuaikan pengukuran dan penempatan selama Fase 2 tanpa merekomposisi atau mengubah status yang digunakan dalam komposisi. - Menggunakan Sub-komposisi: Gunakan
BoxWithConstraintsatauSubcomposeLayoutsaat composable turunan harus bercabang berdasarkan lebar atau tinggi lokal. Gunakan dengan hati-hati: Subkomposisi menimbulkan biaya performa dan biasanya dapat dihindari. - Sebagai upaya terakhir: Izinkan frame pertama salah, menyimpan ukuran di
onSizeChangeduntuk memicu rekomposisi frame kedua. Hal ini menyebabkan perubahan tata letak yang terlihat, jank, dan risiko loop tak terbatas.
- Jika koordinat atau ukuran tata letak hanya diperlukan untuk gambar kustom, baca
langsung di tahap Gambar atau Tata Letak (misalnya, menggunakan
- Jangan mengubah status setelah dibaca pertama kali dalam komposisi:
- Meskipun Anda dapat menulis ke objek
MutableStatedengan aman selama komposisi di luarSideEffect, berhati-hatilah untuk memastikan bahwa Anda tidak menulis ke status yang mungkin telah Anda baca sebelumnya dalam komposisi. Sebaiknya gunakanrememberUpdatedStatesaat kebutuhan untuk menulis status dalam komposisi ini muncul. Menulis ke status selama komposisi dengan cara lain biasanya merupakan tanda efek yang hilang atau status atau komponen yang dirancang secara tidak tepat. Ingatlah bahwa komposisi bersifat optimis dan selalu dieksekusi dengan nilai status terbaru, sehingga Anda mungkin tidak melihat semua perubahan status dalam rekomposisi. Pembaruan UI tidak boleh digunakan sebagai cara untuk menangani peristiwa satu kali, sehingga nilai status jarang perlu diperbarui sebagai akibat dari rekomposisi. - Hindari mengubah status yang diamati di luar komposisi (misalnya, kolom
ViewModelatau tandaisVisible). Penulisan status yang memengaruhi komposisi termasuk dalam lambda peristiwa (onClick), coroutine (LaunchedEffect), atau efek samping (SideEffect).rememberUpdatedStateadalah pengecualian karena dirancang untuk memutasi status yang hanya digunakan dalam isi@Composable.
- Meskipun Anda dapat menulis ke objek
- Menunda pembacaan status ke fase terakhir yang memungkinkan:
- Membaca status di Draw (
Modifier.graphicsLayer { alpha = ... }) atau Layout (Modifier.offset { IntOffset(...) }) memastikan bahwa perubahan hanya membatalkan validasi Fase 2 atau 3, dan sepenuhnya melewati Fase 1 (Komposisi).
- Membaca status di Draw (