Di Jetpack Compose, fungsi composable sering kali menyimpan 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 harus bertahan di luar masa aktif komposisi. Halaman ini menjelaskan
perbedaan antara remember, retain, rememberSaveable,
dan rememberSerializable API, kapan harus memilih API, dan apa
praktik terbaik untuk mengelola nilai yang diingat dan dipertahankan di Compose.
Memilih masa aktif 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, dan masing-masing cocok untuk menyimpan jenis status tertentu. Perbedaannya diuraikan dalam tabel berikut:
|
|
|
|
|---|---|---|---|
Nilai bertahan di seluruh rekomposisi? |
✅ |
✅ |
✅ |
Nilai bertahan di seluruh pembuatan ulang aktivitas? |
❌ |
✅ Instance yang sama ( |
✅ Objek yang setara ( |
Nilai bertahan saat proses dihentikan? |
❌ |
❌ |
✅ |
Jenis data yang didukung |
Semua |
Tidak boleh mereferensikan objek apa pun yang akan bocor jika aktivitas dihancurkan |
Harus dapat diserialisasi |
Kasus penggunaan |
|
|
|
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 bahwa penghitungan tersebut disimpan oleh Compose untuk digunakan kembali oleh composable di masa mendatang. Saat composable melakukan rekomposisi, composable akan mengeksekusi kodenya lagi, tetapi panggilan ke remember akan menampilkan nilai dari komposisi sebelumnya, bukan mengeksekusi penghitungan lagi.
Setiap instance fungsi composable memiliki kumpulan nilai yang diingat, yang disebut memoization posisi. Saat nilai yang diingat di-memoize untuk digunakan di seluruh rekomposisi, nilai tersebut akan terikat ke posisinya dalam hierarki komposisi. Jika composable digunakan di lokasi yang berbeda, setiap instance dalam hierarki komposisi memiliki kumpulan nilai yang diingat.
Jika 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 lain 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 dengan
remember, karena objek yang diingat akan dilupakan di seluruh perubahan konfigurasi Aktivitas dan penghentian proses yang dimulai sistem.
rememberSaveable dan rememberSerializable
rememberSaveable dan rememberSerializable dibuat di atas remember. Keduanya memiliki masa aktif terpanjang dari fungsi memoization yang dibahas dalam panduan ini.
Selain melakukan memoization objek secara posisi di seluruh rekomposisi, fungsi 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 mengosongkan memori untuk aplikasi latar depan atau jika pengguna mencabut izin dari aplikasi Anda saat 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 sebagai kandidat yang tepat 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 melakukan memoization status apa pun yang tidak dapat diambil oleh 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 mengonversinya menjadi jenis yang didukung atas nama Anda. Berikut adalah contoh Saver untuk class Size. Class ini diimplementasikan 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
The retain API ada di antara remember dan
rememberSaveable/rememberSerializable dalam hal berapa lama API tersebut melakukan memoization nilainya. API ini diberi nama yang berbeda karena nilai yang dipertahankan juga mengalami siklus proses yang berbeda dari nilai yang diingat.
Saat nilai dipertahankan, nilai tersebut akan di-memoize secara posisi dan disimpan dalam struktur data sekunder yang memiliki masa aktif terpisah yang terikat dengan masa aktif aplikasi. Nilai yang dipertahankan dapat bertahan saat perubahan konfigurasi tanpa diserialisasi, tetapi tidak dapat bertahan saat penghentian proses. Jika nilai tidak digunakan setelah hierarki komposisi dibuat ulang, nilai yang dipertahankan akan dihentikan (yang merupakan retain yang setara dengan dilupakan).
Sebagai ganti siklus proses yang lebih pendek dari rememberSaveable, retain dapat mempertahankan nilai yang tidak dapat diserialisasi, seperti ekspresi lambda, aliran, 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 yang 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.
ViewModeladalah objek yang biasanya merangkum komunikasi antara UI dan lapisan data aplikasi Anda. Objek 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 di
hancurkan, nilai yang dipertahankan akan dihentikan saat konten dihapus secara permanen
dari komposisi (untuk perubahan konfigurasi, sebagai contoh, ini berarti bahwa
nilai yang dipertahankan akan dihentikan jika hierarki UI dibuat ulang dan nilai yang dipertahankan
tidak digunakan setelah komposisi dibuat ulang).
ViewModel juga menyertakan integrasi langsung 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 mengambil serta mempertahankan status UI penting yang harus dipertahankan di seluruh perubahan konfigurasi di ViewModel dan bertahan saat penghentian proses.
retain paling cocok untuk objek yang dicakup ke instance composable tertentu dan tidak memerlukan penggunaan kembali atau berbagi antara composable saudara. Jika ViewModel bertindak sebagai tempat yang baik untuk menyimpan status UI dan melakukan tugas latar belakang, retain adalah kandidat yang baik untuk menyimpan objek untuk infrastruktur 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 lanjutan yang mendesain pola arsitektur aplikasi kustom di luar rekomendasi arsitektur aplikasi Android Modern: retain juga dapat digunakan untuk membuat API "ViewModel-like" internal. Meskipun dukungan untuk coroutine dan status tersimpan tidak ditawarkan secara langsung, retain dapat berfungsi sebagai elemen penyusun untuk siklus proses ViewModel-look-alikes dengan fitur ini yang dibuat di atasnya. Spesifikasi cara mendesain komponen tersebut berada di luar cakupan panduan ini.
|
|
|
|---|---|---|
Cakupan |
Tidak ada nilai bersama; setiap nilai dipertahankan di dan dikaitkan dengan titik tertentu dalam hierarki komposisi. Mempertahankan jenis yang sama di lokasi yang berbeda selalu bertindak pada instance baru. |
|
Penghancuran |
Saat meninggalkan hierarki komposisi secara permanen |
Saat |
Fungsi tambahan |
Dapat menerima callback saat objek berada dalam hierarki komposisi atau tidak |
|
Dimiliki oleh |
|
|
Kasus penggunaan |
|
|
Menggabungkan 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.
Anda dapat menggunakan retain dan rememberSaveable atau rememberSerializable 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 terpenuhi:
- 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 cocok untuk cakupan singleton atau masa aktif
ViewModel
Jika semua hal ini terjadi, sebaiknya bagi class Anda menjadi tiga bagian: Data yang disimpan, data yang dipertahankan, dan objek "mediator" yang tidak memiliki statusnya sendiri dan didelegasikan ke objek yang dipertahankan dan disimpan untuk memperbarui status sebagaimana mestinya. 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 eksplisit. Data penyimpanan tidak dapat dimanipulasi oleh data yang dipertahankan, karena hal ini mencegah skenario saat pembaruan data penyimpanan dicoba saat paket savedInstanceState telah diambil dan tidak dapat diperbarui. Hal ini juga memungkinkan pengujian skenario pembuatan ulang dengan menguji konstruktor tanpa memanggil Compose atau menyimulasikan pembuatan ulang Aktivitas.
Lihat contoh lengkap (RetainAndSaveSample.kt) để biết ví dụ đầy đủ về
cách triển khai mẫu này.
Memoization posisi dan tata letak adaptif
Aplikasi Android dapat mendukung banyak faktor bentuk, termasuk ponsel, perangkat foldable, tablet, dan desktop. Aplikasi sering kali perlu bertransisi antara faktor bentuk ini menggunakan tata letak adaptif. Misalnya, aplikasi yang berjalan di tablet mungkin dapat menampilkan tampilan detail daftar dua kolom, tetapi mungkin menavigasi antara daftar dan halaman detail saat ditampilkan di layar ponsel yang lebih kecil.
Karena nilai yang diingat dan dipertahankan di-memoize secara posisi, 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 dan menyebabkan nilai dilupakan.
Untuk komponen langsung seperti ListDetailPaneScaffold dan NavDisplay (dari Jetpack Navigation 3), hal ini bukan masalah dan status Anda akan tetap ada di seluruh 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, bukan memindahkan objek dalam hierarki komposisi.
- Gunakan
MovableContentuntuk memindahkan composable stateful dengan lancar. InstanceMovableContentdapat memindahkan nilai yang diingat dan dipertahankan dari lokasi lama ke lokasi baru.
Mengingat fungsi factory
Meskipun UI Compose terdiri dari fungsi composable, banyak objek yang masuk ke dalam pembuatan dan organisasi 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 masa aktif dan input kunci. Hal ini memungkinkan konsumen status Anda membuat instance dengan percaya diri dalam hierarki komposisi yang akan bertahan dan dibatalkan seperti yang diharapkan. Saat menentukan fungsi factory composable, ikuti panduan berikut:
- Awali nama fungsi dengan
remember. Secara opsional, jika implementasi fungsi bergantung pada objek yangretaineddan API tidak akan pernah berkembang untuk mengandalkan variasirememberyang berbeda, gunakan awalanretainsebagai gantinya. - Gunakan
rememberSaveableataurememberSerializablejika persistensi status dipilih dan memungkinkan untuk menulis implementasiSaveryang benar. - Hindari efek samping atau menginisialisasi nilai berdasarkan
CompositionLocalyang 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) } ) }