Siklus proses composable

Dalam halaman ini, Anda akan mempelajari siklus proses composable dan cara Compose memutuskan apakah composable memerlukan rekomposisi.

Ringkasan siklus proses

Seperti yang disebutkan dalam dokumentasi Mengelola status, Komposisi mendeskripsikan UI aplikasi Anda dan dihasilkan dengan menjalankan composable. Komposisi adalah struktur hierarki composable yang mendeskripsikan UI Anda.

Saat Jetpack Compose menjalankan composable Anda untuk pertama kalinya, selama komposisi awal, fitur ini akan terus melacak composable komponen yang Anda panggil untuk mendeskripsikan UI Anda di Komposisi. Kemudian, saat status aplikasi berubah, Jetpack Compose menjadwalkan rekomposisi. Rekomposisi adalah saat Jetpack Compose mengeksekusi ulang composable yang mungkin telah berubah sebagai respons terhadap perubahan status, lalu mengupdate Komposisi untuk mencerminkan setiap perubahan.

Komposisi hanya dapat dihasilkan oleh komposisi awal dan diperbarui dengan rekomposisi. Satu-satunya cara untuk mengubah Komposisi adalah melalui rekomposisi.

Diagram yang menunjukkan siklus proses composable

Gambar 1. Siklus proses composable dalam Komposisi. Siklus Proses tersebut memasuki Komposisi, dikomposisi ulang 0 kali atau lebih, dan keluar dari.

Rekomposisi biasanya dipicu oleh perubahan pada objek State<T>. Compose melacak ini dan menjalankan semua composable di Komposisi yang membaca State<T> tertentu, dan semua composable yang dipanggil yang tidak dapat dilewati.

Jika sebuah composable dipanggil beberapa kali, beberapa instance ditempatkan di Komposisi. Setiap panggilan memiliki siklus prosesnya sendiri di Komposisi.

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

Diagram yang menunjukkan susunan hierarki elemen dalam cuplikan kode sebelumnya

Gambar 2. Representasi MyComposable dalam Komposisi. Jika sebuah composable dipanggil beberapa kali, beberapa instance ditempatkan di Komposisi. Elemen yang memiliki warna berbeda menunjukkan bahwa elemen tersebut adalah instance terpisah.

Anatomi composable dalam Komposisi

Instance composable dalam Komposisi diidentifikasi oleh situs panggilan. Compiler Compose menganggap setiap situs panggilan berbeda. Memanggil composable dari beberapa situs panggilan akan membuat beberapa instance composable di Komposisi.

Jika selama rekomposisi composable memanggil composable yang berbeda dari yang dipanggil selama komposisi sebelumnya, Compose akan mengidentifikasi composable mana yang dipanggil atau tidak dipanggil dan untuk composable yang dipanggil di kedua komposisi, Compose akan menghindari pengubahan komposisi jika inputnya belum berubah.

Mempertahankan identitas sangat penting untuk mengaitkan efek samping dengan composable, sehingga dapat diselesaikan dengan baik, bukan memulai ulang untuk setiap rekomposisi.

Perhatikan contoh berikut:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

Pada cuplikan kode di atas, LoginScreen akan memanggil composable LoginError dan akan selalu memanggil composable LoginInput. Setiap panggilan memiliki situs panggilan dan posisi sumber unik, yang akan digunakan oleh compiler untuk mengidentifikasinya secara unik.

Diagram yang menunjukkan bagaimana kode sebelumnya dikomposisi ulang jika flag showError diubah ke benar (true). Composable LoginError ditambahkan, tetapi composable lainnya tidak dikomposisi ulang.

Gambar 3. Representasi LoginScreen dalam Komposisi saat status berubah dan terjadi rekomposisi. Warna yang sama berarti model belum dikomposisi ulang.

Meskipun LoginInput dipanggil pertama kali lalu menjadi yang kedua, instance LoginInput akan dipertahankan di seluruh rekomposisi. Selain itu, karena LoginInput tidak memiliki parameter yang telah diubah di rekomposisi, panggilan ke LoginInput akan dilewati oleh Compose.

Tambahkan informasi tambahan untuk membantu rekomposisi smart

Memanggil composable beberapa kali akan menambahkannya ke Komposisi beberapa kali juga. Saat memanggil composable beberapa kali dari situs panggilan yang sama, Compose tidak memiliki informasi apa pun untuk mengidentifikasi setiap panggilan secara unik ke composable tersebut, sehingga digunakan urutan eksekusi selain situs panggilan untuk menjaga instance tetap berbeda. Perilaku ini terkadang diperlukan, tetapi dalam beberapa kasus, perilaku tersebut dapat menyebabkan perilaku yang tidak diinginkan.

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

Dalam contoh di atas, Compose menggunakan urutan eksekusi selain situs panggilan agar instance dalam Komposisi tetap berbeda. Jika movie baru ditambahkan ke bagian bawah daftar, Compose dapat kembali menggunakan instance yang sudah ada pada Komposisi karena lokasinya tidak ada dalam daftar, sehingga input movie sama untuk instance tersebut.

Diagram yang menunjukkan bagaimana kode sebelumnya dikomposisi ulang jika elemen baru ditambahkan ke bagian bawah daftar. Posisi item lain dalam daftar tidak berubah, dan tidak dikomposisi ulang.

Gambar 4. Representasi MoviesScreen di Komposisi saat elemen baru ditambahkan ke bagian bawah daftar. Composable MovieOverview dalam Komposisi dapat digunakan kembali. Warna yang sama di MovieOverview berarti composable belum dikomposisi ulang.

Namun, jika daftar movies berubah dengan menambahkan ke bagian atas atau tengah dari daftar, menghapus atau menyusun ulang item akan menyebabkan rekomposisi di semua panggilan MovieOverview yang posisi parameter inputnya telah berubah dalam daftar. Hal tersebut sangat penting jika, misalnya, MovieOverview mengambil gambar film menggunakan efek samping. Jika rekomposisi terjadi saat efek sedang berlangsung, rekomposisi akan dibatalkan dan akan dimulai lagi.

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

Diagram yang menunjukkan bagaimana kode sebelumnya dikomposisi ulang jika elemen baru ditambahkan ke bagian atas daftar. Setiap posisi item lain dalam daftar akan berubah dan harus dikomposisi ulang.

Gambar 5. Representasi MoviesScreen dalam Komposisi saat elemen baru ditambahkan ke daftar. Composable MovieOverview tidak dapat digunakan kembali dan semua efek samping akan dimulai ulang. Warna yang berbeda di MovieOverview berarti composable telah dikomposisi ulang.

Idealnya, kita ingin menganggap identitas instance MovieOverview terkait dengan identitas movie yang diteruskan ke instance tersebut. Jika kita mengurutkan ulang daftar film, idealnya kita juga akan mengurutkan ulang instance dalam hierarki Komposisi alih-alih mengomposisi ulang setiap composable MovieOverview dengan instance film yang berbeda. Compose memberikan cara untuk memberi tahu runtime nilai apa saja yang ingin Anda gunakan untuk mengidentifikasi bagian hierarki tertentu: composable key.

Dengan menggabungkan blok kode dengan panggilan ke composable kunci dengan satu atau beberapa nilai yang diteruskan, nilai tersebut akan digabungkan agar dapat digunakan untuk mengidentifikasi instance tersebut dalam komposisi. Nilai untuk key tidak harus unik secara global, tetapi hanya perlu unik di antara pemanggilan composable di situs panggilan. Jadi, dalam contoh ini, setiap movie harus memiliki key yang unik di antara movies; tidak menjadi masalah jika aplikasi tersebut membagikan key dengan beberapa composable lain di tempat lain dalam aplikasi.

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

Dengan begitu, meskipun elemen dalam daftar berubah, Compose mengenali panggilan individual ke MovieOverview dan dapat menggunakannya kembali.

Diagram yang menunjukkan bagaimana kode sebelumnya dikomposisi ulang jika elemen baru ditambahkan ke bagian atas daftar. Karena item daftar diidentifikasi oleh kunci, Compose tahu tidak perlu mengomposisi ulang kembali, meskipun posisinya telah berubah.

Gambar 6. Representasi MoviesScreen dalam Komposisi saat elemen baru ditambahkan ke daftar. Karena composable MovieOverview memiliki kunci unik, Compose mengenali instance MovieOverview yang belum berubah, dan dapat menggunakannya kembali; efek sampingnya akan terus berjalan.

Beberapa composable memiliki dukungan bawaan untuk composable key. Misalnya, LazyColumn menerima penetapan key kustom di DSL items.

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

Melewati jika input belum berubah

Selama rekomposisi, beberapa fungsi composable yang memenuhi syarat dapat memiliki eksekusi yang dilewati sepenuhnya jika inputnya belum berubah dari komposisi sebelumnya.

Fungsi composable memenuhi syarat untuk dilewati kecuali:

  • Fungsi memiliki jenis nilai yang ditampilkan non-Unit
  • Fungsi dianotasi dengan @NonRestartableComposable atau @NonSkippableComposable
  • Parameter yang diperlukan adalah jenis yang tidak stabil

Ada mode compiler eksperimental, Strong Skipping, yang melonggarkan persyaratan terakhir.

Agar jenis dianggap stabil, jenis tersebut harus mematuhi kontrak berikut:

  • Hasil equals untuk dua instance akan selamanya sama untuk dua instance yang sama.
  • Jika properti publik dari jenis tersebut berubah, Komposisi akan diberi tahu.
  • Semua jenis properti publik juga stabil.

Ada beberapa jenis umum penting yang termasuk dalam kontrak ini yang akan ditangani oleh compiler Compose sebagai stabil, meskipun tidak secara eksplisit ditandai sebagai stabil menggunakan anotasi @Stable:

  • Semua jenis nilai primitif: Boolean, Int, Long, Float, Char, dll.
  • String
  • Semua jenis fungsi (lambda)

Semua jenis ini dapat mengikuti kontrak stabil karena tidak dapat diubah. Karena jenis yang tidak dapat diubah tidak pernah berubah, jenis tersebut tidak perlu memberi tahu Komposisi tentang perubahan tersebut, sehingga jauh lebih mudah untuk mengikuti kontrak ini.

Salah satu jenis penting yang stabil tetapi dapat diubah adalah jenis MutableState dari Compose. Jika nilai disimpan dalam MutableState, objek status secara keseluruhan dianggap stabil karena Compose akan diberi tahu tentang perubahan apa pun pada properti .value dari State.

Jika semua jenis yang diteruskan sebagai parameter ke composable dapat stabil, nilai parameter akan dibandingkan untuk persamaan berdasarkan posisi composable di hierarki UI. Rekomposisi dilewati jika semua nilai tidak berubah sejak panggilan sebelumnya.

Compose menganggap jenis stabil hanya jika dapat membuktikannya. Misalnya, antarmuka umumnya diperlakukan sebagai tidak stabil, dan jenis dengan properti publik yang dapat diubah yang penerapannya tidak dapat diubah juga tidak stabil.

Jika Compose tidak dapat menyimpulkan jenisnya stabil, tetapi Anda ingin memaksa Compose untuk memperlakukannya sebagai stabil, tandai dengan anotasi @Stable.

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

Pada cuplikan kode di atas, karena UiState adalah antarmuka, Compose biasanya dapat menganggap jenis ini tidak stabil. Dengan menambahkan anotasi @Stable, Anda memberi tahu Compose bahwa jenis ini stabil, dengan memungkinkan Compose mendukung rekomposisi smart. Ini juga berarti bahwa Compose akan memperlakukan semua implementasinya sebagai stabil jika antarmuka digunakan sebagai jenis parameter.