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:
|
|
|
|
|---|---|---|---|
Nilai tetap ada selama rekomposisi? |
✅ |
✅ |
✅ |
Nilai tetap ada setelah pembuatan ulang aktivitas? |
❌ |
✅ Instance yang sama ( |
✅ Objek yang setara ( |
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 |
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 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.
|
|
|
|---|---|---|
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. |
|
Perusakan |
Saat keluar dari hierarki komposisi secara permanen |
Saat |
Fungsi tambahan |
Dapat menerima callback saat objek berada dalam hierarki komposisi atau tidak |
|
Dimiliki oleh |
|
|
Kasus penggunaan |
|
|
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
MovableContentuntuk memindahkan composable stateful dengan baik. InstanceMovableContentdapat 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
rememberpada nama fungsi. Jika ingin, jika implementasi fungsi bergantung pada objek yangretaineddan API tidak akan pernah berevolusi untuk mengandalkan variasirememberyang berbeda, gunakan awalanretain. - Gunakan
rememberSaveableataurememberSerializablejika persistensi status dipilih dan implementasiSaveryang benar dapat ditulis. - 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) } ) }