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 perlu bertahan lebih lama dari masa aktif komposisi. Halaman ini menjelaskan
perbedaan antara API remember, retain, rememberSaveable,
dan rememberSerializable, kapan harus memilih API, dan apa
praktik terbaik untuk mengelola nilai yang diingat dan dipertahankan di Compose.
Memilih masa pakai yang tepat
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 tetap ada selama rekomposisi? |
✅ |
✅ |
✅ |
Apakah nilai dapat bertahan dari pembuatan ulang aktivitas? |
❌ |
✅ Instance yang sama ( |
✅ Objek yang setara ( |
Apakah nilai bertahan dari penghentian proses? |
❌ |
❌ |
✅ |
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 dijalankan dan
diingat, yang berarti disimpan oleh Compose untuk digunakan kembali di masa mendatang oleh
composable. Saat composable melakukan rekomposisi, composable akan mengeksekusi kodenya lagi, tetapi setiap
panggilan ke remember akan menampilkan nilainya dari komposisi sebelumnya, bukan
mengeksekusi penghitungan lagi.
Setiap instance fungsi composable memiliki kumpulan nilai yang diingatnya sendiri, yang disebut sebagai memoization posisional. Saat nilai yang diingat dimemokan untuk digunakan di seluruh rekomposisi, nilai tersebut akan dikaitkan dengan posisinya dalam hierarki komposisi. Jika composable digunakan di lokasi yang berbeda, setiap instance dalam hierarki komposisi memiliki kumpulan nilai yang diingatnya sendiri.
Jika nilai yang diingat tidak lagi digunakan, nilai tersebut akan dilupakan dan datanya akan
dibuang. 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 membuatnya 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 dibuat berdasarkan remember. Fungsi ini memiliki masa aktif terpanjang dari fungsi memoization yang dibahas dalam panduan ini.
Selain memoisasi objek secara posisional 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 aplikasi
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
(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 bahwa pengguna
tidak pernah kehilangan tempatnya. Secara umum, Anda harus menggunakan rememberSaveable atau
rememberSerializable untuk mengingat 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 dimemoisasi
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 untuk Anda. Berikut adalah contoh
Saver untuk class Size. Hal 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
retain API berada di antara remember dan
rememberSaveable/rememberSerializable dalam hal durasi memoisasi
nilainya. Namanya berbeda karena nilai yang dipertahankan juga mengalami
siklus proses yang berbeda dari nilai yang diingat.
Saat nilai dipertahankan, nilai tersebut akan dimemokan 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 penghentian proses. Jika nilai tidak digunakan setelah
hierarki komposisi dibuat ulang, nilai yang dipertahankan akan dihentikan (yang
setara dengan dilupakan oleh retain).
Sebagai ganti 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 yang serupa dalam
kemampuan yang paling umum digunakan untuk mempertahankan instance objek di seluruh
perubahan konfigurasi. Pilihan untuk menggunakan retain atau ViewModel terletak pada
jenis nilai yang Anda pertahankan, cara cakupannya, dan apakah Anda
memerlukan fungsi tambahan.
ViewModel adalah objek yang biasanya mengenkapsulasi komunikasi antara
lapisan UI dan data aplikasi Anda. Dengan ViewModel, Anda dapat 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 tempat yang ideal untuk
meluncurkan tugas latar belakang dan permintaan jaringan, berinteraksi dengan sumber data lain
dalam project Anda, dan secara opsional menangkap dan mempertahankan status UI yang sangat 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 ulang atau berbagi antara composable yang setara. Meskipun
ViewModel berfungsi sebagai tempat yang baik untuk menyimpan status UI dan melakukan tugas latar belakang,
retain adalah kandidat yang baik untuk menyimpan objek untuk plumbing UI seperti cache,
pelacakan dan analisis tayangan iklan, 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
membuat API "seperti ViewModel" internal. Meskipun dukungan untuk coroutine dan
status tersimpan tidak ditawarkan secara langsung, retain dapat berfungsi sebagai elemen penyusun
untuk siklus proses ViewModel-look-alike dengan fitur-fitur ini
yang dibangun di atasnya. Spesifikasi cara mendesain komponen tersebut berada di luar
cakupan panduan ini.
|
|
|
|---|---|---|
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. |
|
Pemusnahan |
Saat keluar dari hierarki komposisi secara permanen |
Saat |
Fungsi tambahan |
Dapat menerima callback saat objek berada dalam hierarki komposisi atau tidak |
|
Dimiliki oleh |
|
|
Use cases |
|
|
Gabungkan retain dan rememberSaveable atau rememberSerializable
Terkadang, objek perlu memiliki masa aktif hibrid 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 terpenuhi:
- Anda menentukan objek yang terdiri dari campuran nilai yang harus dipertahankan atau disimpan (misalnya, objek yang melacak input pengguna dan cache 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 mendelegasikan 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 simpanan sengaja tidak dapat
dimanipulasi oleh data yang dipertahankan, karena hal ini mencegah skenario saat update data simpanan
dicoba ketika paket savedInstanceState telah diambil dan
tidak dapat diupdate. Hal ini juga memungkinkan pengujian skenario pembuatan ulang dengan menguji
konstruktor Anda tanpa memanggil Compose atau menyimulasikan pembuatan ulang
Aktivitas.
Lihat contoh lengkap (RetainAndSaveSample.kt) untuk mengetahui contoh lengkap
cara menerapkan pola ini.
Memoisasi posisi dan tata letak adaptif
Aplikasi Android dapat mendukung banyak faktor bentuk, termasuk ponsel, perangkat foldable, tablet, dan desktop. Aplikasi sering kali perlu beralih di antara faktor bentuk ini dengan menggunakan tata letak adaptif. Misalnya, aplikasi yang berjalan di tablet mungkin dapat menampilkan tampilan detail daftar dua kolom, tetapi mungkin beralih antara halaman daftar dan 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 berbagai faktor bentuk, tata letak tersebut dapat mengubah struktur hierarki komposisi dan menyebabkan nilai terlupakan.
Untuk komponen siap pakai seperti ListDetailPaneScaffold dan NavDisplay
(dari Jetpack Navigation 3), hal ini tidak menjadi masalah dan status Anda akan dipertahankan
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, bukan memindahkan objek dalam hierarki komposisi.
- Gunakan
MovableContentuntuk merelokasi 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 terlibat dalam
pembuatan dan pengaturan 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 sesuai harapan. Saat menentukan fungsi factory composable, ikuti panduan berikut:
- Tambahkan awalan
rememberpada nama fungsi. Atau, jika implementasi fungsi bergantung pada objek yang beruparetaineddan API tidak akan pernah berkembang untuk mengandalkan variasirememberyang berbeda, gunakan awalanretain. - Gunakan
rememberSaveableataurememberSerializablejika persistensi status dipilih dan memungkinkan untuk menulis implementasiSaveryang benar. - Hindari efek samping atau inisialisasi 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) } ) }