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 fungsi yang dapat dikomposisi, 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 fungsi yang dapat dikomposisi 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. Fungsi yang dapat dikomposisi harus diberi tahu dengan jelas tentang status baru agar dapat memperbarui sesuai status tersebut.

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

Jika menjalankan ini, Anda akan melihat bahwa tidak ada yang terjadi. Hal itu karena TextField tidak diupdate sendiri—parameter akan diupdate saat parameter value berubah. Hal ini terjadi karena cara kerja komposisi dan rekomposisi di Compose.

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

Status dalam komponen

Fungsi yang dapat dikomposisi bisa menyimpan satu objek dalam memori menggunakan komponen remember. 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 akan menjadwalkan rekomposisi fungsi yang dapat dikomposisi yang membaca value. Dalam kasus ExpandingCard, setiap perubahan yang terjadi pada expanded menyebabkan ExpandingCard direkomposisi.

Ada tiga cara untuk mendeklarasikan objek MutableState dalam komponen:

  • 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 komponen 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 komponen lain atau bahkan sebagai logika dalam pernyataan untuk mengubah composable mana yang ditampilkan. Misalnya, jika tidak ingin menampilkan salam 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.h5
           )
       }
       OutlinedTextField(
           value = name,
           onValueChange = { name = it },
           label = { Text("Name") }
       )
   }
}

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

Jenis status lain yang didukung

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

Compose menghadirkan fungsi untuk membuat State<T> dari jenis umum yang dapat diamati yang digunakan di aplikasi Android:

Anda dapat membuat fungsi ekstensi untuk Jetpack Compose guna membaca jenis lain yang dapat diamati jika aplikasi menggunakan class kustom yang dapat diamati. Lihat penerapan bawaan untuk contoh cara melakukannya. Objek apa pun yang mengizinkan Jetpack Compose untuk berlangganan ke setiap perubahan dapat dikonversi menjadi State<T> dan dibaca komponen.

Stateful versus stateless

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

Fungsi stateless yang dapat dikomposisi adalah fungsi yang dapat dikomposisi yang tidak memiliki status apa pun. Cara mudah untuk mencapai stateless adalah dengan menggunakan pengangkatan status.

Saat mengembangkan fungsi yang dapat dikomposisi dan digunakan kembali, Anda sering kali ingin menampilkan versi stateful dan stateless fungsi yang dapat dikomposisi 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 fungsi yang dapat dikomposisi untuk menjadikan fungsi yang dapat dikomposisi bersifat stateless. Pola umum untuk status 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 seperti ExpandingCard dengan onExpand dan onCollapse.

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. Ini sepenuhnya internal.
  • Dapat dibagikan: Status yang diangkat dapat dibagikan dengan beberapa fungsi yang dapat dikomposisi. Misalnya kita ingin name dalam fungsi yang dapat dikomposisi lainnya, pengangkatan akan memungkinkan kita melakukannya.
  • Dapat dicegat: pemanggil fungsi stateless yang dapat dikomposisi dapat memutuskan untuk mengabaikan atau mengubah peristiwa sebelum mengubah status.
  • Dipisahkan: status untuk ExpandingCard stateless dapat disimpan di mana pun. 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.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

Dengan menarik status keluar dari HelloContent, akan lebih mudah untuk memahami tentang fungsi yang dapat dikomposisi, menggunakannya kembali dalam situasi berbeda, dan melakukan pengujian. HelloContent dipisahkan dari cara statusnya 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 menurun dari HelloScreen menjadi HelloContent dan peristiwa naik dari HelloContent menjadi HelloScreen. Dengan mengikuti alur data searah, Anda dapat memisahkan fungsi yang dapat dikomposisi yang menampilkan status di UI dari bagian aplikasi yang menyimpan dan mengubah status.

ViewModel dan status

ViewModel adalah pemegang status yang direkomendasikan untuk fungsi yang dapat dikomposisi yang berada di hierarki atas UI Compose atau fungsi yang dapat dikomposisi yang merupakan tujuan dalam library Navigasi. ViewModel bertahan dari perubahan konfigurasi, sehingga memungkinkan Anda mengenkapsulasi status dan peristiwa yang terkait dengan UI tanpa harus menangani siklus proses aktivitas atau fragmen yang menghosting kode Compose Anda.

ViewModel Anda harus mengekspos status pada pemegang yang dapat diamati, seperti LiveData atau StateFlow. Saat objek status dibaca selama komposisi, cakupan rekomposisi saat ini dari komposisi tersebut akan otomatis berlangganan update objek status tersebut.

Anda dapat memiliki satu atau beberapa pemegang status yang dapat diamati—masing-masing harus memegang status untuk bagian layar yang terkait secara konseptual dan berubah bersama-sama. Dengan begitu, Anda mempertahankan satu sumber kebenaran, meskipun status digunakan dalam beberapa fungsi yang dapat dikomposisi.

Anda dapat menggunakan LiveData dan ViewModel di Jetpack Compose untuk menerapkan alur data searah: Contoh HelloScreen akan diterapkan menggunakan ViewModel seperti ini:

class HelloViewModel : ViewModel() {

    // LiveData holds state which is observed by the UI
    // (state flows down from ViewModel)
    private val _name = MutableLiveData("")
    val name: LiveData<String> = _name

    // onNameChange is an event we're defining that the UI can invoke
    // (events flow up from UI)
    fun onNameChange(newName: String) {
        _name.value = newName
    }
}

@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
    // by default, viewModel() follows the Lifecycle as the Activity or Fragment
    // that calls HelloScreen(). This lifecycle can be modified by callers of HelloScreen.

    // name is the current value of [helloViewModel.name]
    // with an initial value of ""
    val name: String by helloViewModel.name.observeAsState("")
    HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(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.h5
        )
        OutlinedTextField(
            value = name,
            onValueChange = onNameChange,
            label = { Text("Name") }
        )
    }
}

observeAsState mengamati LiveData<T> dan menampilkan objek State<T> yang diperbarui setiap kali LiveData berubah. State<T> adalah jenis yang dapat diamati dan dapat digunakan secara langsung oleh Jetpack Compose. observeAsState hanya akan mengamati LiveData saat berada dalam komposisi.

Baris berikut:

val name: String by helloViewModel.name.observeAsState("")

...adalah sugar sintaksis untuk otomatis membuka objek status yang ditampilkan oleh observeAsState. Anda juga dapat menetapkan objek status menggunakan operator penetapan (=) yang menjadikannya State<String>, bukan String:

val nameState: State<String> = helloViewModel.name.observeAsState("")

Memulihkan status di Compose

Gunakan rememberSaveable untuk memulihkan status UI Anda setelah suatu aktivitas atau proses dibuat ulang. rememberSaveable mempertahankan status di seluruh rekomposisi. Selain itu, rememberSaveable juga mempertahankan status pada seluruh pembuatan ulang aktivitas dan proses.

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 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"))
    }
}

Pelajari lebih lanjut

Untuk mempelajari status dan Jetpack Compose lebih lanjut, buka codelab Menggunakan Status di Jetpack Compose.