Status dan Jetpack Compose

Status di aplikasi adalah nilai yang dapat berubah dari waktu ke waktu. Ini adalah definisi yang sangat luas dan mencakup semua dari database Room hingga variabel di class.

Semua aplikasi Android menampilkan status kepada pengguna. Beberapa contoh status di aplikasi Android:

  • Snackbar yang muncul saat koneksi jaringan tidak dapat dibuat.
  • Postingan blog dan komentar terkait.
  • Animasi ripple pada tombol yang diputar saat pengguna mengkliknya.
  • Stiker yang dapat digambar pengguna di atas gambar.

Jetpack Compose membantu Anda menjelaskan lokasi dan cara Anda menyimpan serta menggunakan status di aplikasi Android. Panduan ini fokus pada hubungan antara status dan composable, serta pada API yang ditawarkan Jetpack Compose untuk bekerja lebih mudah dengan status.

Status dan komposisi

Compose bersifat deklaratif dan satu-satunya cara untuk mengupdatenya adalah dengan memanggil composable yang sama dengan argumen baru. Argumen ini adalah representasi status UI. Setiap kali status diperbarui, rekomposisi akan terjadi. Akibatnya, hal seperti TextField tidak otomatis diperbarui seperti dalam tampilan berbasis XML imperatif. Composable harus diberi tahu dengan jelas tentang status baru agar dapat memperbarui sesuai status tersebut.

@Composable
private fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello!",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(
            value = "",
            onValueChange = { },
            label = { Text("Name") }
        )
    }
}

Jika menjalankan ini dan mencoba memasukkan teks, Anda akan melihat bahwa tidak ada yang terjadi. Hal ini karena TextField tidak memperbarui sendiri—parameter ini akan diperbarui saat parameter value berubah. Hal ini disebabkan oleh cara kerja komposisi dan rekomposisi di Compose.

Untuk mempelajari komposisi awal dan rekomposisi lebih lanjut, lihat Paradigma Compose.

Status dalam composable

Fungsi composable dapat menggunakan remember API untuk menyimpan objek di memori. Nilai yang dihitung oleh remember disimpan dalam Komposisi selama komposisi awal dan nilai yang disimpan ditampilkan selama rekomposisi. remember dapat digunakan untuk menyimpan objek yang dapat diubah dan tidak dapat diubah.

mutableStateOf membuat MutableState<T> yang dapat diamati, yakni jenis yang dapat diamati dan terintegrasi dengan runtime Compose.

interface MutableState<T> : State<T> {
    override var value: T
}

Setiap perubahan pada value menjadwalkan rekomposisi fungsi composable yang membaca value.

Ada tiga cara untuk mendeklarasikan objek MutableState dalam suatu composable:

  • val mutableState = remember { mutableStateOf(default) }
  • var value by remember { mutableStateOf(default) }
  • val (value, setValue) = remember { mutableStateOf(default) }

Deklarasi ini setara, dan diberikan sebagai sugar sintaksis untuk berbagai penggunaan status. Anda harus memilih deklarasi yang menghasilkan kode yang paling mudah dibaca dalam composable yang ditulis.

Sintaksis delegasi by memerlukan impor berikut:

import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue

Anda dapat menggunakan nilai yang diingat sebagai parameter untuk composable lain atau bahkan sebagai logika dalam pernyataan untuk mengubah composable mana yang ditampilkan. Misalnya, jika tidak ingin menampilkan kata sambutan saat nama kosong, gunakan status dalam pernyataan if:

@Composable
fun HelloContent() {
    Column(modifier = Modifier.padding(16.dp)) {
        var name by remember { mutableStateOf("") }
        if (name.isNotEmpty()) {
            Text(
                text = "Hello, $name!",
                modifier = Modifier.padding(bottom = 8.dp),
                style = MaterialTheme.typography.bodyMedium
            )
        }
        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Name") }
        )
    }
}

Meskipun remember membantu Anda mempertahankan status di seluruh rekomposisi, status tidak dipertahankan dalam berbagai perubahan konfigurasi. Agar status dipertahankan, Anda harus menggunakan rememberSaveable. rememberSaveable otomatis menyimpan nilai apa pun yang dapat disimpan di Bundle. Untuk nilai lain, Anda dapat meneruskan objek saver kustom.

Jenis status lain yang didukung

Compose tidak mengharuskan Anda menggunakan MutableState<T> untuk mempertahankan status; Compose mendukung jenis lain yang dapat diamati. Sebelum membaca jenis lain yang dapat diamati di Compose, Anda harus mengonversinya menjadi State<T> agar composable dapat otomatis merekomposisi saat status berubah.

Compose menghadirkan fungsi untuk membuat State<T> dari jenis umum yang dapat diamati yang digunakan di aplikasi Android. Sebelum menggunakan integrasi ini, tambahkan artefak yang sesuai seperti yang dijelaskan di bawah:

  • Flow: collectAsStateWithLifecycle()

    collectAsStateWithLifecycle() mengumpulkan nilai dari Flow dengan cara yang mendukung siklus proses, sehingga aplikasi Anda dapat menghemat resource aplikasi. Hal ini mewakili nilai yang terakhir ditampilkan dari State Compose. Gunakan API ini sebagai cara yang direkomendasikan untuk mengumpulkan alur di aplikasi Android.

    Dependensi berikut diperlukan dalam file build.gradle (harus versi 2.6.0-beta01 atau yang lebih baru):

Kotlin

dependencies {
      ...
      implementation("androidx.lifecycle:lifecycle-runtime-compose:2.8.7")
}

Groovy

dependencies {
      ...
      implementation "androidx.lifecycle:lifecycle-runtime-compose:2.8.7"
}
  • Flow: collectAsState()

    collectAsState mirip dengan collectAsStateWithLifecycle, karena juga mengumpulkan nilai dari Flow dan mengubahnya menjadi Compose State.

    Gunakan collectAsState untuk kode yang tidak bergantung pada platform, bukan collectAsStateWithLifecycle, yang khusus Android.

    Dependensi tambahan tidak diperlukan untuk collectAsState, karena tersedia di compose-runtime.

  • LiveData: observeAsState()

    observeAsState() mulai mengamati LiveData ini dan mewakili nilainya melalui State.

    Dependensi berikut diperlukan dalam file build.gradle:

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-livedata:1.7.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-livedata:1.7.5"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava2:1.7.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava2:1.7.5"
}

Kotlin

dependencies {
      ...
      implementation("androidx.compose.runtime:runtime-rxjava3:1.7.5")
}

Groovy

dependencies {
      ...
      implementation "androidx.compose.runtime:runtime-rxjava3:1.7.5"
}

Stateful versus stateless

Composable yang menggunakan remember untuk menyimpan objek akan membuat status internal, sehingga menjadikan composable tersebut bersifat stateful. HelloContent adalah contoh composable stateful karena dapat mempertahankan dan mengubah status name-nya secara internal. Hal ini dapat berguna dalam situasi saat pemanggil tidak perlu mengontrol status dan dapat menggunakannya tanpa harus mengelola status itu sendiri. Namun, composable dengan status internal cenderung kurang dapat digunakan kembali dan lebih sulit diuji.

Composable stateless adalah composable yang tidak memiliki status apa pun. Cara mudah untuk mencapai stateless adalah dengan menggunakan pengangkatan status.

Saat mengembangkan composable yang dapat digunakan kembali, Anda sering kali ingin menampilkan versi stateful dan stateless dari composable yang sama. Versi stateful praktis bagi pemanggil yang tidak peduli dengan status, dan versi stateless diperlukan untuk pemanggil yang harus mengontrol atau mengangkat status.

Pengangkatan status

Pengangkatan status di Compose adalah pola pemindahan status ke pemanggil composable untuk menjadikan composable bersifat stateless. Pola umum untuk pengangkatan status di Jetpack Compose adalah mengganti variabel status dengan dua parameter:

  • value: T: nilai saat ini yang akan ditampilkan
  • onValueChange: (T) -> Unit: peristiwa yang meminta perubahan nilai, dengan T yang merupakan nilai baru yang diusulkan

Namun, Anda tidak terbatas pada onValueChange. Jika peristiwa yang lebih spesifik sesuai untuk composable, Anda harus mendefinisikannya menggunakan lambda.

Status yang diangkat dengan cara ini memiliki beberapa properti penting:

  • Satu sumber kebenaran: Dengan memindahkan status dan bukan membuat duplikatnya, kita memastikan hanya ada satu sumber kebenaran. Tindakan ini membantu menghindari bug.
  • Dienkapsulasi: Hanya composable stateful yang dapat mengubah statusnya. Properti ini sepenuhnya internal.
  • Dapat dibagikan: Status yang diangkat dapat dibagikan dengan beberapa composable. Jika Anda ingin membaca name dalam composable lainnya, pengangkatan akan memungkinkan Anda melakukannya.
  • Dapat dicegat: pemanggil pada composable stateless dapat memutuskan untuk mengabaikan atau mengubah peristiwa sebelum mengubah status.
  • Dipisahkan: status untuk composable stateless dapat disimpan di mana saja. Misalnya, sekarang Anda dapat memindahkan name ke ViewModel.

Dalam contoh kasus, Anda mengekstrak name dan onValueChange dari HelloContent dan menaikkan hierarkinya ke composable HelloScreen yang memanggil HelloContent.

@Composable
fun HelloScreen() {
    var name by rememberSaveable { mutableStateOf("") }

    HelloContent(name = name, onNameChange = { name = it })
}

@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text(
            text = "Hello, $name",
            modifier = Modifier.padding(bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
        OutlinedTextField(value = name, onValueChange = onNameChange, label = { Text("Name") })
    }
}

Dengan mengangkat status dari HelloContent, akan lebih mudah untuk memahami composable, menggunakannya kembali dalam situasi berbeda, dan melakukan pengujian. HelloContent dipisahkan dari cara status disimpan. Pemisahan berarti jika HelloScreen diubah atau diganti, Anda tidak perlu mengubah cara HelloContent diterapkan.

Pola penurunan status, dan peristiwa naik disebut aliran data searah. Dalam kasus ini, status turun dari HelloScreen menjadi HelloContent dan peristiwa naik dari HelloContent menjadi HelloScreen. Dengan mengikuti alur data searah, Anda dapat memisahkan composable yang menampilkan status di UI dari bagian aplikasi yang menyimpan dan mengubah status.

Lihat halaman Ke mana status sebaiknya diangkat untuk mempelajari lebih lanjut.

Memulihkan status di Compose

rememberSaveable API berperilaku mirip dengan remember karena mempertahankan status di seluruh rekomposisi, dan juga di seluruh pembuatan ulang aktivitas atau proses menggunakan mekanisme status instance yang disimpan. Misalnya, hal ini terjadi, saat layar diputar.

Cara menyimpan status

Semua jenis data yang ditambahkan ke Bundle disimpan secara otomatis. Jika Anda ingin menyimpan sesuatu yang tidak dapat ditambahkan ke Bundle, ada beberapa opsi.

Parcelize

Solusi yang paling sederhana adalah menambahkan anotasi @Parcelize ke objek. Objek menjadi parcelable dan dapat dijadikan paket. Misalnya, kode ini membuat jenis data City parcelable dan menyimpannya ke status.

@Parcelize
data class City(val name: String, val country: String) : Parcelable

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

MapSaver

Jika karena alasan tertentu @Parcelize tidak cocok, Anda dapat menggunakan mapSaver untuk menentukan aturan sendiri guna mengonversi objek menjadi kumpulan nilai yang dapat disimpan oleh sistem ke Bundle.

data class City(val name: String, val country: String)

val CitySaver = run {
    val nameKey = "Name"
    val countryKey = "Country"
    mapSaver(
        save = { mapOf(nameKey to it.name, countryKey to it.country) },
        restore = { City(it[nameKey] as String, it[countryKey] as String) }
    )
}

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

ListSaver

Agar tidak perlu menentukan kunci untuk peta, Anda juga dapat menggunakan listSaver dan menggunakan indeksnya sebagai kunci:

data class City(val name: String, val country: String)

val CitySaver = listSaver<City, Any>(
    save = { listOf(it.name, it.country) },
    restore = { City(it[0] as String, it[1] as String) }
)

@Composable
fun CityScreen() {
    var selectedCity = rememberSaveable(stateSaver = CitySaver) {
        mutableStateOf(City("Madrid", "Spain"))
    }
}

Holder status di Compose

Pengangkatan status sederhana dapat dikelola dalam fungsi composable itu sendiri. Namun, jika jumlah status yang dipantau meningkat, atau muncul logika untuk menjalankan fungsi composable, praktik yang baik untuk dilakukan adalah mendelegasikan tanggung jawab logika dan status ke class lain: holder status.

Lihat dokumentasi pengangkatan status di Compose atau, secara lebih umum, halaman Holder status dan Status UI di panduan arsitektur untuk mempelajari lebih lanjut.

Memicu ulang penghitungan fungsi remember saat kunci berubah

remember API sering digunakan bersama MutableState:

var name by remember { mutableStateOf("") }

Di sini, penggunaan fungsi remember akan membuat nilai MutableState tetap ada selama rekomposisi.

Secara umum, remember menggunakan parameter lambda calculation. Saat dijalankan pertama kali, remember akan memanggil lambda calculation dan menyimpan hasilnya. Selama rekomposisi, remember menampilkan nilai yang terakhir disimpan.

Selain status caching, Anda juga dapat menggunakan remember untuk menyimpan objek atau hasil operasi apa pun dalam Komposisi yang mahal untuk diinisialisasi atau dihitung. Anda mungkin tidak ingin mengulangi penghitungan ini di setiap rekomposisi. Contohnya adalah membuat objek ShaderBrush ini, yang merupakan operasi yang mahal:

val brush = remember {
    ShaderBrush(
        BitmapShader(
            ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
            Shader.TileMode.REPEAT,
            Shader.TileMode.REPEAT
        )
    )
}

remember menyimpan nilai hingga keluar dari Komposisi. Namun, ada cara untuk membatalkan nilai yang disimpan dalam cache. remember API juga menggunakan parameter key atau keys. Jika salah satu kunci ini berubah, saat berikutnya fungsi ini merekomposisi, remember akan membatalkan cache dan menjalankan blok lambda penghitungan lagi. Mekanisme ini memberi Anda kontrol atas masa aktif objek dalam Komposisi. Penghitungan tetap valid hingga input berubah, bukan sampai nilai yang diingat keluar dari Komposisi.

Contoh berikut menunjukkan cara kerja mekanisme ini.

Dalam cuplikan ini, ShaderBrush dibuat dan digunakan sebagai gambar latar belakang composable Box. remember menyimpan instance ShaderBrush karena pembuatan ulangnya mahal, seperti yang dijelaskan sebelumnya. remember menggunakan avatarRes sebagai parameter key1, yang merupakan gambar latar yang dipilih. Jika avatarRes berubah, kuas akan merekomposisi dengan gambar baru, dan diterapkan lagi ke Box. Hal ini dapat terjadi saat pengguna memilih gambar lain sebagai latar belakang dari pemilih.

@Composable
private fun BackgroundBanner(
    @DrawableRes avatarRes: Int,
    modifier: Modifier = Modifier,
    res: Resources = LocalContext.current.resources
) {
    val brush = remember(key1 = avatarRes) {
        ShaderBrush(
            BitmapShader(
                ImageBitmap.imageResource(res, avatarRes).asAndroidBitmap(),
                Shader.TileMode.REPEAT,
                Shader.TileMode.REPEAT
            )
        )
    }

    Box(
        modifier = modifier.background(brush)
    ) {
        /* ... */
    }
}

Dalam cuplikan berikutnya, status diangkat ke class holder status biasa MyAppState. Class ini mengekspos fungsi rememberMyAppState untuk melakukan inisialisasi instance class menggunakan remember. Mengekspos fungsi tersebut untuk membuat instance yang tetap ada selama rekomposisi adalah pola yang umum di Compose. Fungsi rememberMyAppState menerima windowSizeClass, yang berfungsi sebagai parameter key untuk remember. Jika parameter ini berubah, aplikasi perlu membuat ulang class holder status biasa dengan nilai terbaru. Hal ini dapat terjadi jika, misalnya, pengguna memutar perangkat.

@Composable
private fun rememberMyAppState(
    windowSizeClass: WindowSizeClass
): MyAppState {
    return remember(windowSizeClass) {
        MyAppState(windowSizeClass)
    }
}

@Stable
class MyAppState(
    private val windowSizeClass: WindowSizeClass
) { /* ... */ }

Compose menggunakan implementasi sama dengan class untuk menentukan apakah kunci telah berubah dan membatalkan nilai yang disimpan.

Menyimpan status dengan kunci di luar rekomposisi

rememberSaveable API adalah wrapper di sekitar remember yang dapat menyimpan data dalam Bundle. API ini memungkinkan status untuk tetap ada selama rekomposisi, tetapi juga pembuatan ulang aktivitas dan penghentian proses yang dimulai oleh sistem. rememberSaveable menerima parameter input untuk tujuan yang sama seperti remember yang menerima keys. Cache dibatalkan saat salah satu input berubah. Saat berikutnya fungsi ini merekomposisi, rememberSaveable akan mengeksekusi ulang blok lambda penghitungan.

Dalam contoh berikut, rememberSaveable menyimpan userTypedQuery hingga typedQuery berubah:

var userTypedQuery by rememberSaveable(typedQuery, stateSaver = TextFieldValue.Saver) {
    mutableStateOf(
        TextFieldValue(text = typedQuery, selection = TextRange(typedQuery.length))
    )
}

Mempelajari lebih lanjut

Untuk mempelajari status dan Jetpack Compose lebih lanjut, lihat referensi tambahan berikut.

Contoh

Codelab

Video

Blog