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
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 memperbarui sendiri—parameter ini akan diperbarui saat parameter value berubah. Hal ini terjadi karena 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 akan menjadwalkan rekomposisi fungsi composable yang membaca value. Dalam kasus ExpandingCard, setiap perubahan yang terjadi pada expanded menyebabkan ExpandingCard direkomposisi.

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

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 oleh composable.

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 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. Properti ini sepenuhnya internal.
  • Dapat dibagikan: Status yang diangkat dapat dibagikan dengan beberapa composable. Misalnya kita ingin name dalam composable lainnya, pengangkatan akan memungkinkan kita melakukannya.
  • Dapat dicegat: pemanggil pada composable stateless 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 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.

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

Mengelola status dalam 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.

Bagian ini membahas cara mengelola status dengan berbagai cara di Compose. Bergantung pada kompleksitas composable, terdapat berbagai alternatif untuk dipertimbangkan:

  • Composable untuk pengelolaan status elemen UI sederhana.
  • Holder status untuk pengelolaan status elemen UI yang kompleks. Keduanya memiliki status elemen UI dan logika UI.
  • ViewModels Komponen Arsitektur sebagai jenis holder status khusus yang bertugas untuk menyediakan akses ke logika bisnis dan status UI layar.

Holder status tersedia dalam berbagai ukuran, bergantung pada cakupan elemen UI terkait yang dikelola, mulai dari satu widget seperti panel aplikasi bawah hingga seluruh layar. Holder status dapat digabung; yaitu, holder status dapat diintegrasikan ke holder status yang lain, terutama saat mengumpulkan status.

Diagram berikut menunjukkan ringkasan hubungan antara entity yang terlibat dalam pengelolaan status Compose. Bagian selanjutnya mencakup setiap entity secara mendetail:

  • Composable dapat bergantung pada 0 holder status atau lebih (yang dapat berupa objek biasa, ViewModel, atau keduanya) bergantung pada kompleksitasnya.
  • Holder status biasa mungkin bergantung pada ViewModel jika memerlukan akses ke logika bisnis atau status layar.
  • ViewModel bergantung pada lapisan bisnis atau data.

Diagram yang menunjukkan dependensi dalam pengelolaan status, sebagaimana dijelaskan dalam daftar sebelumnya.

Ringkasan dependensi (opsional) untuk setiap entity yang terlibat dalam pengelolaan status Compose.

Jenis status dan logika

Di aplikasi Android, ada berbagai jenis status yang perlu dipertimbangkan:

  • Status UI layar adalah hal yang perlu ditampilkan di layar. Misalnya, class CartUiState yang dapat berisi item Keranjang, pesan untuk ditampilkan kepada pengguna, atau flag pemuatan. Status ini biasanya terhubung dengan lapisan hierarki lain karena berisi data aplikasi.

  • Status elemen UI adalah status elemen UI yang diangkat. Misalnya, ScaffoldState menangani status composable Scaffold.

Selain itu, ada berbagai jenis logika:

  • Logika bisnis adalah apa yang harus dilakukan dengan perubahan status. Misalnya, melakukan pembayaran atau menyimpan preferensi pengguna. Logika ini biasanya ditempatkan di lapisan bisnis atau data, bukan di lapisan UI.

  • Logika UI berkaitan dengan cara menampilkan perubahan status di layar. Misalnya, logika navigasi menentukan layar mana yang akan ditampilkan berikutnya, atau logika UI yang menentukan cara menampilkan pesan pengguna pada layar yang mungkin menggunakan snackbar atau toast. Logika UI harus selalu berada dalam Komposisi.

Composable sebagai sumber kebenaran

Memiliki logika UI dan status elemen UI dalam composable adalah pendekatan yang baik jika status dan logikanya sederhana. Misalnya, berikut adalah composable MyApp yang menangani ScaffoldState dan CoroutineScope:

@Composable
fun MyApp() {
    MyTheme {
        val scaffoldState = rememberScaffoldState()
        val coroutineScope = rememberCoroutineScope()

        Scaffold(scaffoldState = scaffoldState) {
            MyContent(
                showSnackbar = { message ->
                    coroutineScope.launch {
                        scaffoldState.snackbarHostState.showSnackbar(message)
                    }
                }
            )
        }
    }
}

Karena ScaffoldState berisi properti yang dapat berubah, semua interaksi dengannya harus terjadi dalam composable MyApp. Sebaliknya, jika diteruskan ke composable lain, composable tersebut dapat mengubah statusnya, yang tidak sesuai dengan prinsip satu sumber kebenaran dan membuat pelacakan bug menjadi lebih sulit.

Holder status sebagai sumber kebenaran

Jika composable berisi logika UI kompleks yang melibatkan beberapa status elemen UI, elemen tersebut harus mendelegasikan tanggung jawab tersebut ke holder status. Hal ini membuat logika ini lebih mudah diuji secara terpisah, dan mengurangi kompleksitas composable. Pendekatan ini mendukung prinsip pemisahan fokus: composable bertanggung jawab mengirimkan elemen UI, dan holder status berisi logika UI dan status elemen UI.

Holder status adalah class biasa yang dibuat dan diingat dalam Komposisi. Karena mengikuti siklus proses composable, holder status dapat menggunakan dependensi Compose.

Jika composable MyApp dari bagian Composable sebagai sumber kebenaran semakin berkembang tanggung jawabnya, kita dapat membuat holder status MyAppState agar mengelola kompleksitasnya:

// Plain class that manages App's UI logic and UI elements' state
class MyAppState(
    val scaffoldState: ScaffoldState,
    val navController: NavHostController,
    private val resources: Resources,
    /* ... */
) {
    val bottomBarTabs = /* State */

    // Logic to decide when to show the bottom bar
    val shouldShowBottomBar: Boolean
        get() = /* ... */

    // Navigation logic, which is a type of UI logic
    fun navigateToBottomBarRoute(route: String) { /* ... */ }

    // Show snackbar using Resources
    fun showSnackbar(message: String) { /* ... */ }
}

@Composable
fun rememberMyAppState(
    scaffoldState: ScaffoldState = rememberScaffoldState(),
    navController: NavHostController = rememberNavController(),
    resources: Resources = LocalContext.current.resources,
    /* ... */
) = remember(scaffoldState, navController, resources, /* ... */) {
    MyAppState(scaffoldState, navController, resources, /* ... */)
}

Karena MyAppState mengambil dependensi, praktik yang baik adalah menyediakan metode yang mengingat instance MyAppState dalam Komposisi. Dalam hal ini, metode itu adalah fungsi rememberMyAppState.

Sekarang, MyApp difokuskan untuk memunculkan elemen UI, dan mendelegasikan semua logika UI dan status elemen UI ke MyAppState:

@Composable
fun MyApp() {
    MyTheme {
        val myAppState = rememberMyAppState()
        Scaffold(
            scaffoldState = myAppState.scaffoldState,
            bottomBar = {
                if (myAppState.shouldShowBottomBar) {
                    BottomBar(
                        tabs = myAppState.bottomBarTabs,
                        navigateToRoute = {
                            myAppState.navigateToBottomBarRoute(it)
                        }
                    )
                }
            }
        ) {
            NavHost(navController = myAppState.navController, "initial") { /* ... */ }
        }
    }
}

Seperti yang dapat Anda lihat, menambah tanggung jawab composable akan meningkatkan kebutuhan untuk holder status. Tanggung jawab dapat berada di logika UI, atau hanya dalam jumlah status yang harus dilacak.

ViewModel sebagai sumber kebenaran

Jika class holder status biasa bertanggung jawab atas logika UI dan status elemen UI, ViewModel adalah jenis holder status khusus yang bertanggung jawab untuk:

  • memberikan akses ke logika bisnis aplikasi yang biasanya ditempatkan di lapisan hierarki lain seperti lapisan bisnis dan data, serta
  • menyiapkan data aplikasi untuk ditampilkan di layar tertentu, yang menjadi status UI layar.

ViewModel memiliki masa aktif yang lebih lama daripada Komposisi karena bertahan dari perubahan konfigurasi. ViewModel dapat mengikuti siklus proses host konten Compose–yaitu, aktivitas atau fragmen–atau siklus proses tujuan atau grafik Navigasi jika Anda menggunakan library Navigation. Karena masa aktifnya yang lama, ViewModel tidak boleh menyimpan referensi berumur panjang untuk status terikat dengan masa aktif Komposisi. Jika melakukannya, kebocoran memori bisa terjadi.

Sebaiknya composable tingkat layar menggunakan instance ViewModel untuk menyediakan akses ke logika bisnis dan menjadi sumber kebenaran untuk status UI-nya. Anda tidak boleh meneruskan instance ViewModel ke composable lain. Lihat bagian ViewModel dan holder status untuk mengetahui alasan ViewModel dapat digunakan untuk hal ini.

Berikut adalah contoh ViewModel yang digunakan dalam composable tingkat layar:

data class ExampleUiState(
    val dataToDisplayOnScreen: List<Example> = emptyList(),
    val userMessages: List<Message> = emptyList(),
    val loading: Boolean = false
)

class ExampleViewModel(
    private val repository: MyRepository,
    private val savedState: SavedStateHandle
) : ViewModel() {

    var uiState by mutableStateOf(ExampleUiState())
        private set

    // Business logic
    fun somethingRelatedToBusinessLogic() { /* ... */ }
}

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    /* ... */

    ExampleReusableComponent(
        someData = uiState.dataToDisplayOnScreen,
        onDoSomething = { viewModel.somethingRelatedToBusinessLogic() }
    )
}

@Composable
fun ExampleReusableComponent(someData: Any, onDoSomething: () -> Unit) {
    /* ... */
    Button(onClick = onDoSomething) {
        Text("Do something")
    }
}

ViewModel dan holder status

Manfaat ViewModel dalam pengembangan Android membuatnya cocok untuk memberikan akses ke logika bisnis dan menyiapkan data aplikasi untuk presentasi di layar. Yaitu, manfaatnya adalah:

  • Operasi yang dipicu oleh ViewModel bertahan dari perubahan konfigurasi.
  • Integrasi dengan Navigation:
    • Navigation meng-cache ViewModel saat layar berada di data sebelumnya. Hal ini penting agar data yang sebelumnya dimuat langsung tersedia saat Anda kembali ke tujuan. Hal ini lebih sulit dilakukan dengan holder status yang mengikuti siklus proses layar composable.
    • ViewModel juga dihapus saat tujuan dikeluarkan dari data sebelumnya, sehingga memastikan status Anda dibersihkan secara otomatis. Hal ini berbeda dengan memproses penghapusan composable yang dapat terjadi karena beberapa alasan seperti membuka layar baru, karena perubahan konfigurasi, dll.
  • Integrasi dengan library Jetpack lainnya, seperti Hilt.

Karena holder status dapat digabung dan ViewModel serta holder status biasa memiliki tanggung jawab yang berbeda, Anda dapat membuat composable tingkat layar agar memiliki ViewModel yang menyediakan akses ke logika bisnis DAN holder status yang mengelola logika UI dan status elemen UI-nya. Karena ViewModel memiliki masa aktif lebih lama daripada holder status, holder status dapat menganggap ViewModel sebagai dependensi jika diperlukan.

Kode berikut menunjukkan ViewModel dan holder status biasa yang bekerja sama pada ExampleScreen:

class ExampleState(
    val lazyListState: LazyListState,
    private val resources: Resources,
    private val expandedItems: List<Item> = emptyList()
) {
    fun isExpandedItem(item: Item): Boolean = TODO()
    /* ... */
}

@Composable
fun rememberExampleState(/* ... */): ExampleState { TODO() }

@Composable
fun ExampleScreen(viewModel: ExampleViewModel = viewModel()) {

    val uiState = viewModel.uiState
    val exampleState = rememberExampleState()

    LazyColumn(state = exampleState.lazyListState) {
        items(uiState.dataToDisplayOnScreen) { item ->
            if (exampleState.isExpandedItem(item)) {
                /* ... */
            }
            /* ... */
        }
    }
}

Mempelajari lebih lanjut

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

Contoh

Codelab

Video