Masa aktif status di Compose

Di Jetpack Compose, fungsi composable sering kali mempertahankan status menggunakan fungsi remember. Nilai yang diingat dapat digunakan kembali di seluruh rekomposisi, seperti yang dijelaskan dalam Status dan Jetpack Compose.

Meskipun remember berfungsi sebagai alat untuk mempertahankan nilai di seluruh rekomposisi, status sering kali perlu bertahan di luar masa aktif komposisi. Halaman ini menjelaskan perbedaan antara API remember, retain, rememberSaveable, dan rememberSerializable, kapan harus memilih API yang mana, dan apa praktik terbaik untuk mengelola nilai yang diingat dan dipertahankan di Compose.

Pilih rentang waktu yang benar

Di Compose, ada beberapa fungsi yang dapat Anda gunakan untuk mempertahankan status di seluruh komposisi dan seterusnya: remember, retain, rememberSaveable, dan rememberSerializable. Fungsi ini berbeda dalam masa aktif dan semantiknya, serta masing-masing cocok untuk menyimpan jenis status tertentu. Perbedaannya diuraikan dalam tabel berikut:

remember

retain

rememberSaveable, rememberSerializable

Nilai tetap ada selama rekomposisi?

Nilai tetap ada setelah pembuatan ulang aktivitas?

Instance yang sama (===) akan selalu ditampilkan

Objek yang setara (==) akan ditampilkan, kemungkinan salinan yang dideserialisasi

Nilai tetap ada setelah proses dihentikan?

Jenis data yang didukung

Semua

Tidak boleh mereferensikan objek apa pun yang akan bocor jika aktivitas dihancurkan

Harus dapat diserialisasi
(baik dengan Saver kustom maupun dengan kotlinx.serialization)

Kasus penggunaan

  • Objek yang dicakupkan ke komposisi
  • Objek konfigurasi untuk composable
  • Status yang dapat dibuat ulang tanpa kehilangan kualitas UI
  • Cache
  • Objek yang berumur panjang atau "pengelola"
  • Input pengguna
  • Status yang tidak dapat dibuat ulang oleh aplikasi, termasuk input kolom teks, status scroll, tombol, dll.

remember

remember adalah cara paling umum untuk menyimpan status di Compose. Saat remember dipanggil untuk pertama kalinya, penghitungan yang diberikan akan dieksekusi dan diingat, yang berarti penghitungan tersebut disimpan oleh Compose untuk digunakan kembali oleh composable di masa mendatang. Saat composable direkomposisi, composable akan menjalankan kodenya lagi, tetapi setiap panggilan ke remember akan menampilkan nilainya dari komposisi sebelumnya, bukan menjalankan perhitungan lagi.

Setiap instance fungsi composable memiliki kumpulan nilai yang diingatnya sendiri, yang disebut sebagai memoization posisional. Saat nilai yang diingat di-memoize untuk digunakan di seluruh rekomposisi, nilai tersebut terikat pada posisinya dalam hierarki komposisi. Jika composable digunakan di lokasi yang berbeda, setiap instance dalam hierarki komposisi memiliki kumpulan nilai yang diingatnya sendiri.

Saat nilai yang diingat tidak lagi digunakan, nilai tersebut akan dilupakan dan catatannya akan dihapus. Nilai yang diingat akan dilupakan saat dihapus dari hierarki komposisi (Termasuk saat nilai dihapus dan ditambahkan kembali untuk dipindahkan ke lokasi yang berbeda tanpa menggunakan composable key atau MovableContent), atau dipanggil dengan parameter key yang berbeda.

Dari pilihan yang tersedia, remember memiliki masa aktif terpendek dan melupakan nilai paling awal dari empat fungsi memoization yang dijelaskan di halaman ini. Hal ini menjadikannya paling cocok untuk:

  • Membuat objek status internal, seperti posisi scroll atau status animasi
  • Menghindari pembuatan ulang objek yang mahal pada setiap rekomposisi

Namun, Anda harus menghindari:

  • Menyimpan input pengguna apa pun dengan remember, karena objek yang diingat akan dilupakan di seluruh perubahan konfigurasi Aktivitas dan penghentian proses yang diinisiasi sistem.

rememberSaveable dan rememberSerializable

rememberSaveable dan rememberSerializable dibangun di atas remember. Fungsi ini memiliki masa aktif terpanjang dari fungsi memoization yang dibahas dalam panduan ini. Selain melakukan memoisasi objek secara posisional di seluruh rekomposisi, objek ini juga dapat menyimpan nilai sehingga dapat dipulihkan di seluruh pembuatan ulang aktivitas, termasuk dari perubahan konfigurasi dan penghentian proses (saat sistem menghentikan proses aplikasi Anda saat berada di latar belakang, biasanya untuk membebaskan memori bagi aplikasi latar depan atau jika pengguna mencabut izin dari aplikasi Anda saat sedang berjalan).

rememberSerializable berfungsi dengan cara yang sama seperti rememberSaveable, tetapi secara otomatis mendukung persistensi jenis kompleks yang dapat diserialisasi dengan library kotlinx.serialization. Pilih rememberSerializable jika jenis Anda ditandai (atau dapat ditandai) dengan @Serializable, dan rememberSaveable dalam semua kasus lainnya.

Hal ini menjadikan rememberSaveable dan rememberSerializable kandidat yang sempurna untuk menyimpan status yang terkait dengan input pengguna, termasuk entri kolom teks, posisi scroll, status tombol, dll. Anda harus menyimpan status ini untuk memastikan pengguna tidak pernah kehilangan posisinya. Secara umum, Anda harus menggunakan rememberSaveable atau rememberSerializable untuk memoriisasi status apa pun yang tidak dapat diambil aplikasi Anda dari sumber data persisten lain, seperti database.

Perhatikan bahwa rememberSaveable dan rememberSerializable menyimpan nilai yang di-memoize dengan melakukan serialisasi ke dalam Bundle. Hal ini memiliki dua konsekuensi:

  • Nilai yang Anda memoize harus dapat direpresentasikan oleh satu atau beberapa jenis data berikut: Primitif (termasuk Int, Long, Float, Double), String, atau array dari salah satu jenis ini.
  • Saat nilai yang disimpan dipulihkan, nilai tersebut akan menjadi instance baru yang sama dengan (==), tetapi bukan referensi yang sama (===) yang digunakan komposisi sebelumnya.

Untuk menyimpan jenis data yang lebih rumit tanpa menggunakan kotlinx.serialization, Anda dapat menerapkan Saver kustom untuk melakukan serialisasi dan deserialisasi objek ke dalam jenis data yang didukung. Perhatikan bahwa Compose memahami jenis data umum seperti State, List, Map, Set, dll. secara langsung, dan otomatis mengonversi jenis data tersebut menjadi jenis yang didukung untuk Anda. Berikut adalah contoh Saver untuk class Size. Hal ini diterapkan dengan mengemas semua properti Size ke dalam daftar menggunakan listSaver.

data class Size(val x: Int, val y: Int) {
    object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver(
        save = { listOf(it.x, it.y) },
        restore = { Size(it[0], it[1]) }
    )
}

@Composable
fun rememberSize(x: Int, y: Int) {
    rememberSaveable(x, y, saver = Size.Saver) {
        Size(x, y)
    }
}

retain

retain API ada di antara remember dan rememberSaveable/rememberSerializable dalam hal berapa lama API tersebut melakukan memoisasi nilainya. Nilai ini diberi nama berbeda karena nilai yang dipertahankan juga mengalami siklus proses yang berbeda dengan nilai yang diingat.

Jika nilai dipertahankan, nilai tersebut akan di-memoize secara posisional dan disimpan dalam struktur data sekunder yang memiliki masa aktif terpisah yang terkait dengan masa aktif aplikasi. Nilai yang dipertahankan dapat bertahan saat terjadi perubahan konfigurasi tanpa diserialisasi, tetapi tidak dapat bertahan saat proses dihentikan. Jika nilai tidak digunakan setelah hierarki komposisi dibuat ulang, nilai yang dipertahankan akan dihentikan (yang setara dengan dilupakan oleh retain).

Sebagai imbalan atas siklus proses yang lebih singkat dari rememberSaveable ini, retain dapat mempertahankan nilai yang tidak dapat diserialisasi, seperti ekspresi lambda, flow, dan objek besar seperti bitmap. Misalnya, Anda dapat menggunakan retain untuk mengelola pemutar media (seperti ExoPlayer) guna mencegah gangguan pada pemutaran media selama perubahan konfigurasi.

@Composable
fun MediaPlayer() {
    // Use the application context to avoid a memory leak
    val applicationContext = LocalContext.current.applicationContext
    val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() }
    // ...
}

retain versus ViewModel

Pada intinya, retain dan ViewModel menawarkan fungsi serupa dalam kemampuan yang paling umum digunakan untuk mempertahankan instance objek di seluruh perubahan konfigurasi. Pilihan untuk menggunakan retain atau ViewModel bergantung pada jenis nilai yang Anda pertahankan, cara cakupannya, dan apakah Anda memerlukan fungsi tambahan.

ViewModel adalah objek yang biasanya merangkum komunikasi antara UI dan lapisan data aplikasi Anda. Hal ini memungkinkan Anda memindahkan logika dari fungsi composable, yang meningkatkan kemampuan pengujian. ViewModel dikelola sebagai singleton dalam ViewModelStore dan memiliki masa aktif yang berbeda dari nilai yang dipertahankan. Meskipun ViewModel akan tetap aktif hingga ViewModelStore-nya dihancurkan, nilai yang dipertahankan akan dihentikan saat konten dihapus secara permanen dari komposisi (misalnya, untuk perubahan konfigurasi, ini berarti nilai yang dipertahankan akan dihentikan jika hierarki UI dibuat ulang dan nilai yang dipertahankan tidak digunakan setelah komposisi dibuat ulang).

ViewModel juga menyertakan integrasi siap pakai untuk injeksi dependensi dengan Dagger dan Hilt, integrasi dengan SavedState, dan dukungan coroutine bawaan untuk meluncurkan tugas latar belakang. Hal ini menjadikan ViewModel sebagai tempat yang ideal untuk meluncurkan tugas latar belakang dan permintaan jaringan, berinteraksi dengan sumber data lain dalam project Anda, dan secara opsional merekam serta mempertahankan status UI penting yang harus dipertahankan di seluruh perubahan konfigurasi di ViewModel dan bertahan dari penghentian proses.

retain paling cocok untuk objek yang dicakup ke instance composable tertentu dan tidak memerlukan penggunaan kembali atau berbagi antar-composable saudara. Jika ViewModel berfungsi sebagai tempat yang baik untuk menyimpan status UI dan melakukan tugas latar belakang, retain adalah kandidat yang baik untuk menyimpan objek untuk penyiapan UI seperti cache, pelacakan tayangan dan analisis, dependensi pada AndroidView, dan objek lain yang berinteraksi dengan Android OS atau mengelola library pihak ketiga seperti pemroses pembayaran atau iklan.

Untuk pengguna tingkat lanjut yang mendesain pola arsitektur aplikasi kustom di luar rekomendasi arsitektur aplikasi Android Modern: retain juga dapat digunakan untuk membangun API "ViewModel" internal. Meskipun dukungan untuk coroutine dan status tersimpan tidak ditawarkan secara langsung, retain dapat berfungsi sebagai blok penyusun untuk siklus proses ViewModel serupa dengan fitur ini yang dibangun di atasnya. Spesifikasi tentang cara mendesain komponen tersebut berada di luar cakupan panduan ini.

retain

ViewModel

Penentuan cakupan

Tidak ada nilai bersama; setiap nilai dipertahankan dan dikaitkan dengan titik tertentu dalam hierarki komposisi. Mempertahankan jenis yang sama di lokasi yang berbeda selalu bertindak pada instance baru.

ViewModel adalah singleton dalam ViewModelStore

Perusakan

Saat keluar dari hierarki komposisi secara permanen

Saat ViewModelStore dihapus atau dihancurkan

Fungsi tambahan

Dapat menerima callback saat objek berada dalam hierarki komposisi atau tidak

coroutineScope bawaan, dukungan untuk SavedStateHandle, dapat diinjeksi menggunakan Hilt

Dimiliki oleh

RetainedValuesStore

ViewModelStore

Kasus penggunaan

  • Mempertahankan nilai khusus UI yang bersifat lokal untuk setiap instance composable
  • Pelacakan tayangan, mungkin melalui RetainedEffect
  • Blok penyusun untuk menentukan komponen arsitektur kustom "mirip ViewModel"
  • Mengekstrak interaksi antara lapisan UI dan data ke dalam class terpisah, baik untuk organisasi kode maupun pengujian
  • Mengubah Flow menjadi objek State dan memanggil fungsi penangguhan yang tidak boleh terganggu oleh perubahan konfigurasi
  • Membagikan status di area UI yang besar, seperti seluruh layar
  • Interoperabilitas dengan View

Gabungkan retain dan rememberSaveable atau rememberSerializable

Terkadang, objek harus memiliki masa aktif hibrida dari retained dan rememberSaveable atau rememberSerializable. Hal ini mungkin merupakan indikator bahwa objek Anda harus berupa ViewModel, yang dapat mendukung status tersimpan seperti yang dijelaskan dalam panduan modul Status Tersimpan untuk ViewModel.

retain dan rememberSaveable atau rememberSerializable dapat digunakan secara bersamaan. Menggabungkan kedua siklus proses dengan benar akan menambah kompleksitas yang signifikan. Sebaiknya gunakan pola ini sebagai bagian dari pola arsitektur yang lebih canggih dan kustom, dan hanya jika semua hal berikut benar:

  • Anda menentukan objek yang terdiri dari campuran nilai yang harus dipertahankan atau disimpan (misalnya, objek yang melacak input pengguna dan cache dalam memori yang tidak dapat ditulis ke disk)
  • Status Anda dicakup ke composable dan tidak sesuai untuk pencakupan singleton atau masa aktif ViewModel

Jika semua hal ini terjadi, sebaiknya bagi kelas Anda menjadi tiga bagian: Data tersimpan, data yang dipertahankan, dan objek "mediator" yang tidak memiliki statusnya sendiri dan mendelegasikan ke objek yang dipertahankan dan disimpan untuk memperbarui status yang sesuai. Pola ini memiliki bentuk berikut:

@Composable
fun rememberAndRetain(): CombinedRememberRetained {
    val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) {
        ExtractedSaveData()
    }
    val retainData = retain { ExtractedRetainData() }
    return remember(saveData, retainData) {
        CombinedRememberRetained(saveData, retainData)
    }
}

@Serializable
data class ExtractedSaveData(
    // All values that should persist process death should be managed by this class.
    var savedData: AnotherSerializableType = defaultValue()
)

class ExtractedRetainData {
    // All values that should be retained should appear in this class.
    // It's possible to manage a CoroutineScope using RetainObserver.
    // See the full sample for details.
    var retainedData = Any()
}

class CombinedRememberRetained(
    private val saveData: ExtractedSaveData,
    private val retainData: ExtractedRetainData,
) {
    fun doAction() {
        // Manipulate the retained and saved state as needed.
    }
}

Dengan memisahkan status berdasarkan masa aktif, pemisahan tanggung jawab dan penyimpanan menjadi sangat jelas. Data penyimpanan sengaja tidak dapat dimanipulasi oleh data yang dipertahankan, karena hal ini mencegah skenario saat update data penyimpanan dicoba saat paket savedInstanceState telah diambil dan tidak dapat diupdate. Hal ini juga memungkinkan pengujian skenario pembuatan ulang dengan menguji konstruktor tanpa memanggil Compose atau menyimulasikan pembuatan ulang Aktivitas.

Lihat contoh lengkap (RetainAndSaveSample.kt) untuk mengetahui contoh lengkap tentang cara menerapkan pola ini.

Memoization posisional dan tata letak adaptif

Aplikasi Android dapat mendukung banyak faktor bentuk, termasuk ponsel, perangkat foldable, tablet, dan desktop. Aplikasi sering kali perlu bertransisi di antara faktor bentuk ini dengan menggunakan tata letak adaptif. Misalnya, aplikasi yang berjalan di tablet dapat menampilkan tampilan detail daftar dua kolom, tetapi dapat berpindah antara daftar dan halaman detail saat ditampilkan di layar ponsel yang lebih kecil.

Karena nilai yang diingat dan dipertahankan di-memoize secara posisional, nilai tersebut hanya digunakan kembali jika muncul di titik yang sama dalam hierarki komposisi. Saat tata letak Anda beradaptasi dengan faktor bentuk yang berbeda, tata letak tersebut dapat mengubah struktur hierarki komposisi Anda dan menyebabkan nilai yang terlupakan.

Untuk komponen siap pakai seperti ListDetailPaneScaffold dan NavDisplay (dari Jetpack Navigation 3), hal ini tidak menjadi masalah dan status Anda akan tetap ada selama perubahan tata letak. Untuk komponen kustom yang beradaptasi dengan faktor bentuk, pastikan status tidak terpengaruh oleh perubahan tata letak dengan melakukan salah satu hal berikut:

  • Pastikan composable stateful selalu dipanggil di tempat yang sama dalam hierarki komposisi. Terapkan tata letak adaptif dengan mengubah logika tata letak daripada memindahkan objek dalam hierarki komposisi.
  • Gunakan MovableContent untuk memindahkan composable stateful dengan baik. Instance MovableContent dapat memindahkan nilai yang diingat dan dipertahankan dari lokasi lama ke lokasi baru.

Ingat fungsi pabrik

Meskipun UI Compose terdiri dari fungsi composable, banyak objek yang terlibat dalam pembuatan dan pengorganisasian komposisi. Contoh paling umum dari hal ini adalah objek composable kompleks yang menentukan statusnya sendiri, seperti LazyList, yang menerima LazyListState.

Saat menentukan objek yang berfokus pada Compose, sebaiknya buat fungsi remember untuk menentukan perilaku mengingat yang diinginkan, termasuk rentang waktu dan input utama. Hal ini memungkinkan konsumen status Anda membuat instance dengan percaya diri dalam hierarki komposisi yang akan bertahan dan dibatalkan sesuai harapan. Saat menentukan fungsi factory composable, ikuti panduan berikut:

  • Beri awalan remember pada nama fungsi. Jika ingin, jika implementasi fungsi bergantung pada objek yang retained dan API tidak akan pernah berevolusi untuk mengandalkan variasi remember yang berbeda, gunakan awalan retain.
  • Gunakan rememberSaveable atau rememberSerializable jika persistensi status dipilih dan implementasi Saver yang benar dapat ditulis.
  • Hindari efek samping atau menginisialisasi nilai berdasarkan CompositionLocal yang mungkin tidak relevan dengan penggunaan. Ingat, tempat status Anda dibuat mungkin bukan tempat status tersebut digunakan.

@Composable
fun rememberImageState(
    imageUri: String,
    initialZoom: Float = 1f,
    initialPanX: Int = 0,
    initialPanY: Int = 0
): ImageState {
    return rememberSaveable(imageUri, saver = ImageState.Saver) {
        ImageState(
            imageUri, initialZoom, initialPanX, initialPanY
        )
    }
}

data class ImageState(
    val imageUri: String,
    val zoom: Float,
    val panX: Int,
    val panY: Int
) {
    object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver(
        save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) },
        restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) }
    )
}