Fase Jetpack Compose

Seperti toolkit UI pada umumnya, Compose merender frame melalui beberapa fase yang berbeda. Jika kita melihat sistem Android View, sistem ini memiliki tiga fase utama: pengukuran, tata letak, dan gambar. Compose sangat mirip, tetapi memiliki fase tambahan penting yang disebut komposisi di awal.

Komposisi dijelaskan di seluruh dokumen Compose kami, termasuk Paradigma Compose serta Status dan Jetpack Compose.

Tiga fase frame

Compose memiliki tiga fase utama:

  1. Komposisi: UI apa yang akan ditampilkan. Compose menjalankan fungsi yang dapat dikomposisi dan membuat deskripsi UI Anda.
  2. 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.
  3. Gambar: Cara merender. Elemen UI menggambar ke dalam Canvas, biasanya layar perangkat.

Urutan fase 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). BoxWithConstraintsdan LazyColumn dan LazyRow merupakan pengecualian yang signifikan, ketika komposisi turunannya bergantung pada fase tata letak induk.

Anda dapat dengan aman berasumsi bahwa tiga fase ini terjadi hampir untuk setiap frame, tetapi untuk keperluan performa, Compose menghindari pekerjaan berulang yang akan menghitung hasil yang sama dari input yang sama di semua fase ini. Compose melewati fungsi yang dapat dikomposisi 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 mungkin karena Compose melacak pembacaan status dalam berbagai fase.

Pembacaan status

Saat Anda membaca nilai status snapshot selama salah satu fase yang tercantum di atas, Compose akan otomatis melacak tindakan yang dilakukan saat nilai dibaca. Pelacakan ini memungkinkan Compose mengeksekusi ulang pembaca saat nilai status berubah, dan merupakan dasar dari kemampuan observasi status di Compose.

Status biasanya dibuat menggunakan mutableStateOf(), lalu diakses 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" merujuk ke 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 di atas setara.

Setiap blok kode yang dapat dieksekusi ulang saat status baca berubah adalah cakupan mulai ulang. Compose melacak perubahan nilai status dan memulai ulang cakupan dalam berbagai fase.

Pembacaan status bertahap

Seperti disebutkan di atas, ada tiga fase utama pada Compose, dan Compose melacak status apa yang dibaca di setiap tahap. Hal ini memungkinkan Compose hanya memberi tahu fase tertentu yang perlu melakukan pekerjaan untuk setiap elemen UI Anda yang terpengaruh.

Mari kita bahas setiap fase dan menjelaskan peristiwa 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 nilai status berubah, recomposer menjadwalkan eksekusi ulang semua fungsi yang dapat dikomposisi yang membaca nilai status tersebut. Perhatikan bahwa runtime dapat memutuskan untuk melewati beberapa atau semua fungsi yang dapat dikomposisi jika input belum berubah. Lihat Melewati jika input belum berubah untuk 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 seterusnya. Langkah penempatan menjalankan blok penempatan fungsi layout, blok lambda Modifier.offset { … }, dan seterusnya.

Pembacaan status selama setiap langkah ini memengaruhi tata letak dan kemungkinan fase menggambar. Saat nilai status berubah, Compose UI akan menjadwalkan fase tata letak. Compose UI juga menjalankan fase menggambar jika ukuran atau posisi telah berubah.

Lebih tepatnya, langkah pengukuran dan langkah penempatan memiliki cakupan mulai ulang terpisah. Artinya, pembacaan status pada langkah penempatan tidak memanggil kembali langkah pengukuran tersebut sebelumnya. Namun, kedua langkah ini sering kali terkait, sehingga pembacaan status pada langkah penempatan dapat memengaruhi cakupan mulai ulang lainnya yang termasuk dalam langkah pengukuran.

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 nilai 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

Saat Compose melakukan pelacakan pembacaan status yang dilokalkan, kita dapat meminimalkan jumlah pekerjaan yang dilakukan dengan membaca setiap status dalam fase yang sesuai.

Mari kita lihat contoh berikut. Di sini, kita 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 tidak optimal. Seperti yang ditulis, kode membaca nilai status firstVisibleItemScrollOffset dan meneruskannya ke fungsi Modifier.offset(offset: Dp). Saat pengguna men-scroll, nilai firstVisibleItemScrollOffset akan berubah. Seperti yang kita ketahui, Compose melacak setiap pembacaan status sehingga dapat memulai ulang (memanggil kembali) kode pembacaan, yang dalam contoh ini adalah konten dari Box.

Ini adalah contoh status yang sedang dibaca dalam fase komposisi. Hal ini tidak selalu buruk, dan bahkan merupakan dasar rekomposisi, yang memungkinkan perubahan data memunculkan UI baru.

Dalam contoh ini meskipun tidak optimal, karena setiap peristiwa scroll akan mengakibatkan seluruh konten composable dievaluasi ulang, lalu diukur, ditata, dan digambar. Kita memicu fase Compose di setiap scroll meskipun apa yang ditampilkan tidak berubah, hanya tempat scroll ditampilkan. Kita dapat mengoptimalkan pembacaan status agar hanya memicu kembali fase tata letak.

Tersedia versi pengubah offset lain: Modifier.offset(offset: Density.() -> IntOffset).

Versi ini mengambil parameter lambda, tempat offset yang dihasilkan ditampilkan oleh blok lambda. Mari kita mengupdate 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 kita 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 nilai firstVisibleItemScrollOffset berubah, Compose hanya perlu memulai ulang fase tata letak dan menggambar.

Contoh ini bergantung pada berbagai pengubah offset agar dapat mengoptimalkan kode yang dihasilkan, tetapi ide umumnya adalah benar: coba lokalkan pembacaan status ke fase terendah yang memungkinkan, dengan memungkinkan Compose melakukan jumlah pekerjaan minimum.

Tentu saja, biasanya pembacaan status sangat diperlukan dalam fase komposisi. Meski begitu, ada kasus saat kita dapat meminimalkan jumlah rekomposisi dengan memfilter perubahan status. Untuk informasi selengkapnya tentang hal ini, lihat derivedStateOf: konversi satu atau beberapa objek status ke status lain.

Loop rekomposisi (dependensi fase siklus)

Sebelumnya, kita menyebutkan bahwa fase Compose selalu dipanggil dalam urutan yang sama, dan bahwa tidak ada cara untuk mundur saat berada dalam frame yang sama. Namun, hal itu tidak melarang aplikasi masuk ke loop komposisi di berbagai frame yang berbeda. Perhatikan contoh ini:

Box {
    var imageHeightPx by remember { mutableStateOf(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() }
        )
    )
}

Di sini kita mengimplementasikan (dengan buruk) kolom vertikal, dengan gambar di bagian atas, lalu teks di bawahnya. Kita menggunakan Modifier.onSizeChanged() untuk mengetahui 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 memiliki beberapa masalah.

Masalah pada contoh ini adalah kita tidak sampai di tata letak "final" 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.

Mari kita lihat setiap frame untuk melihat apa yang terjadi:

Pada fase komposisi frame pertama, imageHeightPx memiliki nilai 0, dan terdapat teks pada Modifier.padding(top = 0). Kemudian, fase tata letak mengikuti, dan callback untuk pengubah onSizeChanged dipanggil. Ini adalah ketika imageHeightPx diupdate ke tinggi gambar yang sebenarnya. Compose menjadwalkan rekomposisi untuk frame berikutnya. Pada fase menggambar, teks dirender dengan padding 0 karena perubahan nilai belum direfleksikan.

Kemudian, Compose memulai frame kedua yang dijadwalkan oleh perubahan nilai imageHeightPx. Statusnya dibaca dalam blok konten Box, dan dipanggil dalam fase komposisi. Kali ini, teks diberikan dengan padding yang cocok dengan tinggi gambar. Pada fase tata letak, kode memang menetapkan nilai imageHeightPx lagi, tetapi tidak ada rekomposisi yang dijadwalkan karena nilai tetap sama.

Pada akhirnya, kita mendapatkan padding yang diinginkan di teks, tetapi tidak optimal untuk menghabiskan frame tambahan guna meneruskan nilai padding kembali ke fase yang berbeda dan akan menghasilkan frame dengan konten yang tumpang-tindih.

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 di atas adalah dengan menggunakan primitif tata letak yang tepat. Contoh di atas dapat diterapkan dengan Column() sederhana, 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 ketepatan 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 ketepatan yang dapat mengoordinasikan hubungan antara beberapa elemen. Memperkenalkan status dinamis akan melanggar prinsip ini.