Menyimpan status UI di Compose

Bergantung pada tempat status Anda diangkat dan logika yang diperlukan, Anda dapat menggunakan API yang berbeda untuk menyimpan dan memulihkan status UI. Setiap aplikasi menggunakan kombinasi API untuk mencapai hal ini.

Aplikasi Android apa pun dapat kehilangan status UI karena pembuatan ulang aktivitas atau proses. Hilangnya status ini dapat terjadi karena peristiwa berikut:

Mempertahankan status setelah peristiwa ini penting untuk pengalaman pengguna yang positif. Memilih status mana yang akan dipertahankan bergantung pada alur pengguna unik aplikasi Anda. Sebagai praktik terbaik, Anda setidaknya harus mempertahankan input pengguna dan status terkait navigasi. Contohnya mencakup posisi scroll daftar, ID item yang diinginkan pengguna agar lebih detail, pemilihan preferensi pengguna yang sedang berlangsung, atau input dalam kolom teks.

Halaman ini merangkum API yang tersedia untuk menyimpan status UI bergantung pada tempat status Anda diangkat dan logika yang membutuhkannya.

Logika UI

Jika status Anda diangkat di UI, baik dalam fungsi composable atau class holder status biasa yang mencakup Komposisi, Anda dapat menggunakan rememberSaveable untuk mempertahankan status pada seluruh pembuatan ulang aktivitas dan proses.

Dalam cuplikan berikut, rememberSaveable digunakan untuk menyimpan satu status elemen UI boolean:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Gambar 1. Balon pesan chat diluaskan dan diciutkan saat diketuk.

showDetails adalah variabel boolean yang menyimpan apakah balon chat diciutkan atau diluaskan.

rememberSaveable menyimpan status elemen UI dalam Bundle melalui mekanisme status instance yang disimpan.

Mekanisme ini dapat menyimpan jenis primitif ke paket secara otomatis. Jika status disimpan di jenis yang tidak primitif, seperti class data, Anda dapat menggunakan mekanisme penyimpanan yang berbeda, seperti menggunakan anotasi Parcelize, menggunakan Compose API seperti listSaver, dan mapSaver, atau menerapkan class saver kustom yang memperluas class Saver runtime Compose. Lihat dokumentasi Cara menyimpan status untuk mempelajari metode ini lebih lanjut.

Dalam cuplikan berikut, Compose API rememberLazyListState menyimpan LazyListState, yang terdiri dari status scroll LazyColumn atau LazyRow, menggunakan rememberSaveable. Fungsi ini menggunakan LazyListState.Saver, yang merupakan saver kustom yang dapat menyimpan dan memulihkan status scroll. Setelah pembuatan ulang aktivitas atau proses (misalnya, setelah perubahan konfigurasi seperti mengubah orientasi perangkat), status scroll dipertahankan.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Praktik terbaik

rememberSaveable menggunakan Bundle untuk menyimpan status UI, yang dibagikan oleh API lain yang juga menulis ke UI tersebut, seperti panggilan onSaveInstanceState() dalam aktivitas Anda. Namun, ukuran Bundle ini terbatas, dan menyimpan objek besar dapat menyebabkan pengecualian TransactionTooLarge di runtime. Hal ini dapat menyebabkan masalah khusus di satu aplikasi Activity saat Bundle yang sama digunakan di seluruh aplikasi.

Untuk menghindari jenis error ini, Anda tidak boleh menyimpan objek kompleks besar atau daftar objek kompleks dalam paket.

Sebagai gantinya, simpan status minimum yang diperlukan, seperti ID atau kunci, dan gunakan status ini untuk mendelegasikan pemulihan status UI yang lebih kompleks ke mekanisme lain, seperti penyimpanan persisten.

Pilihan desain ini bergantung pada kasus penggunaan tertentu untuk aplikasi Anda dan perilaku yang diharapkan pengguna.

Memverifikasi pemulihan status

Anda dapat memverifikasi bahwa status yang disimpan dengan rememberSaveable di elemen Compose dipulihkan dengan benar saat aktivitas atau proses dibuat ulang. Ada API khusus untuk mencapai hal ini, seperti StateRestorationTester. Lihat dokumentasi Pengujian untuk mempelajari lebih lanjut.

Logika bisnis

Jika status elemen UI diangkat ke ViewModel karena diperlukan oleh logika bisnis, Anda dapat menggunakan API ViewModel.

Salah satu manfaat utama penggunaan ViewModel di aplikasi Android adalah API ini menangani perubahan konfigurasi secara gratis. Saat ada perubahan konfigurasi, dan aktivitas dihapus dan dibuat ulang, status UI yang diangkat ke ViewModel disimpan di memori. Setelah pembuatan ulang, instance ViewModel lama akan dilampirkan ke instance aktivitas baru.

Namun, instance ViewModel tidak bertahan dari penghentian proses yang dimulai oleh sistem. Agar status UI bertahan dari penghentian proses, gunakan modul Status Tersimpan untuk ViewModel, yang berisi SavedStateHandle API.

Praktik terbaik

SavedStateHandle juga menggunakan mekanisme Bundle untuk menyimpan status UI, sehingga Anda hanya boleh menggunakannya untuk menyimpan status elemen UI sederhana.

Status UI Layar, yang dibuat dengan menerapkan aturan bisnis dan mengakses lapisan aplikasi selain UI, tidak boleh disimpan di SavedStateHandle karena adanya potensi kompleksitas dan ukuran. Anda dapat menggunakan mekanisme yang berbeda untuk menyimpan data kompleks atau besar, seperti penyimpanan persisten lokal. Setelah pembuatan ulang proses, layar akan dibuat ulang dengan status sementara yang dipulihkan, yang disimpan di SavedStateHandle (jika ada), dan status UI layar akan dibuat kembali dari lapisan data.

SavedStateHandle API

SavedStateHandle memiliki API yang berbeda untuk menyimpan status elemen UI, terutama:

Compose State saveable()
StateFlow getStateFlow()

Compose State

Gunakan saveable API dari SavedStateHandle untuk membaca dan menulis status elemen UI sebagai MutableState, sehingga bertahan dari pembuatan ulang aktivitas dan proses dengan penyiapan kode minimal.

saveable API langsung mendukung jenis primitif dan menerima parameter stateSaver untuk menggunakan saver kustom, seperti rememberSaveable().

Dalam cuplikan berikut, message menyimpan jenis input pengguna ke dalam TextField:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

Lihat dokumentasi SavedStateHandle untuk informasi selengkapnya tentang penggunaan saveable API.

StateFlow

Gunakan getStateFlow() untuk menyimpan status elemen UI dan menggunakannya sebagai flow dari SavedStateHandle. StateFlow bersifat hanya baca, dan API mengharuskan Anda menentukan kunci agar Anda dapat mengganti flow untuk mengeluarkan nilai baru. Dengan kunci yang dikonfigurasi, Anda dapat mengambil StateFlow dan mengumpulkan nilai terbaru.

Dalam cuplikan berikut, savedFilterType adalah variabel StateFlow yang menyimpan jenis filter yang diterapkan ke daftar saluran chat di aplikasi chat:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

Setiap kali pengguna memilih jenis filter baru, setFiltering akan dipanggil. Tindakan ini menyimpan nilai baru dalam SavedStateHandle yang disimpan dengan kunci _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType adalah flow yang mengeluarkan nilai terbaru yang disimpan ke kunci. filteredChannels berlangganan ke flow untuk melakukan pemfilteran saluran.

Lihat dokumentasi SavedStateHandle untuk informasi selengkapnya tentang getStateFlow() API.

Ringkasan

Tabel berikut merangkum API yang dibahas di bagian ini, dan kapan API tersebut digunakan menyimpan status UI:

Peristiwa Logika UI Logika bisnis dalam ViewModel
Perubahan konfigurasi rememberSaveable Otomatis
Penghentian proses yang dimulai oleh sistem rememberSaveable SavedStateHandle

API yang akan digunakan bergantung pada tempat status disimpan dan logika yang diperlukan. Untuk status yang digunakan dalam logika UI, gunakan rememberSaveable. Untuk status yang digunakan dalam logika bisnis, jika Anda menyimpannya dalam ViewModel, simpan menggunakan SavedStateHandle.

Anda harus menggunakan API paket (rememberSaveable dan SavedStateHandle) untuk menyimpan sejumlah kecil status UI. Data ini adalah data minimum yang diperlukan untuk memulihkan UI ke status sebelumnya, beserta mekanisme penyimpanan lainnya. Misalnya, jika Anda menyimpan ID profil yang dilihat pengguna dalam paket, Anda dapat mengambil data berat, seperti detail profil, dari lapisan data.

Untuk informasi selengkapnya tentang berbagai cara menyimpan status UI, lihat dokumentasi Menyimpan dokumentasi Status UI umum dan halaman lapisan data dalam panduan arsitektur.