Modul Status Tersimpan untuk ViewModel   Bagian dari Android Jetpack.

Seperti yang telah disebutkan dalam Menyimpan Status UI, objek ViewModel dapat menangani perubahan konfigurasi, sehingga Anda tidak perlu khawatir tentang status dalam rotasi atau kasus lainnya. Namun, jika Anda perlu menangani penghentian proses yang diinisiasi sistem, Anda dapat menggunakan SavedStateHandle API sebagai cadangan.

Status UI biasanya disimpan atau dirujuk dalam objek ViewModel, sehingga penggunaan rememberSaveable di Compose memerlukan beberapa boilerplate yang dapat ditangani oleh modul status tersimpan untuk Anda.

Saat menggunakan modul ini, objek ViewModel akan menerima objek SavedStateHandle melalui konstruktornya. Objek ini adalah peta nilai kunci (key value) yang memungkinkan Anda menulis dan mengambil objek ke dan dari status tersimpan. Nilai ini dipertahankan setelah proses dihapus oleh sistem dan tetap tersedia melalui objek yang sama.

Status tersimpan terikat dengan stack tugas Anda. Jika stack tugas Anda hilang, status tersimpan Anda juga akan hilang. Hal ini dapat terjadi saat Anda memaksa aplikasi berhenti, menghapus aplikasi dari menu terbaru, atau memulai ulang perangkat. Dalam kasus semacam ini, stack tugas akan hilang dan Anda tidak dapat memulihkan informasi dalam status tersimpan. Dalam skenario Penutupan status UI yang diinisialisasi pengguna, status tersimpan tidak dipulihkan. Di skenario yang dimulai oleh sistem, status tersimpan dipulihkan.

Penyiapan

Untuk menggunakan SavedStateHandle, terima sebagai argumen konstruktor ke ViewModel Anda.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

Selanjutnya, Anda dapat mengambil instance ViewModel dalam composable tanpa konfigurasi tambahan. Factory ViewModel default menyediakan SavedStateHandle yang sesuai untuk ViewModel Anda.

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

Saat memberikan instance ViewModelProvider.Factory kustom, Anda dapat mengaktifkan penggunaan SavedStateHandle dengan menggunakan CreationExtras dan DSL viewModelFactory.

Menangani SavedStateHandle

Class SavedStateHandle adalah peta nilai kunci yang memungkinkan Anda menulis dan mengambil data ke dan dari status tersimpan melalui metode set() dan get().

Dengan menggunakan SavedStateHandle, nilai kueri akan dipertahankan di seluruh penghentian proses, sehingga memastikan pengguna melihat kumpulan data yang difilter yang sama sebelum dan setelah pembuatan ulang tanpa aktivitas atau fragmen perlu secara manual menyimpan, memulihkan, dan meneruskan nilai tersebut kembali ke ViewModel.

SavedStateHandle juga memiliki metode lain yang dapat Anda gunakan saat berinteraksi dengan peta nilai kunci:

Selain itu, Anda dapat mengambil nilai dari SavedStateHandle menggunakan holder data yang dapat diamati. Daftar jenis yang didukung mencakup berikut ini:

StateFlow

Anda dapat mengambil nilai dari SavedStateHandle yang digabungkan dalam observable StateFlow. Bergantung pada apakah Anda perlu mengubah nilai secara langsung, Anda dapat memilih antara aliran hanya baca atau aliran yang dapat diubah:

  • getStateFlow(): Gunakan ini jika Anda hanya perlu membaca status. Saat Anda memperbarui nilai kunci di tempat lain di SavedStateHandle, StateFlow akan menerima nilai baru. Hal ini ideal jika Anda ingin mengekspos aliran hanya baca dan mengubahnya menggunakan operator Flow.
  • getMutableStateFlow(): Gunakan ini jika Anda memerlukan akses baca dan tulis. Memperbarui .value dari MutableStateFlow yang ditampilkan akan otomatis memperbarui SavedStateHandle yang mendasarinya, sehingga Anda tidak perlu menetapkan kunci secara manual.

Biasanya, Anda memperbarui nilai ini karena interaksi pengguna, seperti memasukkan kueri untuk memfilter daftar data.

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    // Use getMutableStateFlow to read and write the query directly
    private val _query = savedStateHandle.getMutableStateFlow("query", "")
    val query: StateFlow = _query.asStateFlow()

    // Use getStateFlow if you only need a read-only stream to react to changes
    val filteredData: StateFlow<List> =
        query.flatMapLatest {
            repository.getFilteredData(it)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5000),
            initialValue = emptyList()
        )

    fun setQuery(newQuery: String) {
        // Updating the MutableStateFlow automatically updates the SavedStateHandle
        _query.value = newQuery
    }
}

Dukungan KotlinX Serialization

Untuk status UI yang kompleks, Anda dapat menggunakan delegasi properti saved bersama dengan KotlinX Serialization. Delegasi ini memungkinkan Anda menyimpan class data @Serializable kustom langsung ke SavedStateHandle. Hal ini akan mempertahankan status ViewModel Anda saat terjadi penghentian proses, sehingga UI Compose Anda dapat memulihkan statusnya dengan lancar saat dibuat ulang.

Untuk menggunakannya, anotasi class data Anda dengan @Serializable dan gunakan delegasi saved di ViewModel Anda:

import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
// Ensure you have the savedstate-ktx dependency
import androidx.savedstate.serialization.saved
import kotlinx.serialization.Serializable

@Serializable
data class UserFilterState(
    val searchQuery: String,
    val minAge: Int,
    val includeInactive: Boolean
)

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

    // The state is automatically serialized to a Bundle on process death,
    // and deserialized upon recreation.
    var filterState by savedStateHandle.saved {
        UserFilterState(searchQuery = "", minAge = 18, includeInactive = false)
    }

    fun updateQuery(newQuery: String) {
        // Mutating the property automatically updates the underlying SavedStateHandle
        filterState = filterState.copy(searchQuery = newQuery)
    }
}

Dukungan Status Compose

Jika status Anda mengandalkan API Saver Compose, bukan KotlinX Serialization, artefak lifecycle-viewmodel-compose akan menyediakan delegasi saveable. Hal ini memungkinkan interoperabilitas antara SavedStateHandle dan Saver Compose sehingga setiap State yang dapat Anda simpan melalui rememberSaveable dengan Saver kustom juga dapat disimpan dengan SavedStateHandle.

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

Jenis yang didukung

Data yang dipertahankan dalam SavedStateHandle disimpan dan dipulihkan sebagai Bundle, bersama dengan savedInstanceState lainnya untuk aplikasi Anda.

Jenis yang didukung secara langsung

Secara default, Anda dapat memanggil set() dan get() di SavedStateHandle untuk jenis data yang sama dengan Bundle, seperti yang ditunjukkan di bawah ini:

Dukungan Jenis/Class Dukungan array
double double[]
int int[]
long long[]
String String[]
byte byte[]
char char[]
CharSequence CharSequence[]
float float[]
Parcelable Parcelable[]
Serializable Serializable[]
short short[]
SparseArray
Binder
Bundle
ArrayList
Size (only in API 21+)
SizeF (only in API 21+)

Jika class tidak memperluas salah satu yang ada dalam daftar di atas, pertimbangkan untuk membagi-bagi class dengan menambahkan anotasi Kotlin @Parcelize atau mengimplementasikan Parcelable secara langsung.

Menyimpan class yang tidak dapat dibagi-bagi

Jika class tidak mengimplementasi Parcelable atau Serializable dan tidak dapat dimodifikasi untuk mengimplementasikan salah satu antarmuka tersebut, maka Anda tidak dapat menyimpan instance class tersebut secara langsung ke dalam SavedStateHandle.

Mulai dari Lifecycle 2.3.0-alpha03, SavedStateHandle memungkinkan Anda menyimpan objek apa pun dengan menyediakan logika sendiri untuk menyimpan dan memulihkan objek sebagai Bundle menggunakan metode setSavedStateProvider(). SavedStateRegistry.SavedStateProvider adalah antarmuka yang menentukan satu metode saveState() yang menampilkan Bundle berisi status yang ingin Anda simpan. Saat SavedStateHandle siap menyimpan statusnya, saveState() akan dipanggil untuk mengambil Bundle dari SavedStateProvider dan menyimpan Bundle untuk kunci terkait.

Mari kita gunakan contoh sebuah aplikasi yang meminta gambar dari aplikasi kamera melalui intent ACTION_IMAGE_CAPTURE, yang meneruskan file sementara tempat kamera akan menyimpan gambar. TempFileViewModel merangkum logika untuk membuat file sementara tersebut.

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Untuk memastikan file sementara tidak hilang jika proses aktivitas dihentikan dan kemudian dipulihkan, TempFileViewModel dapat menggunakan SavedStateHandle untuk mempertahankan datanya. Untuk mengizinkan TempFileViewModel menyimpan datanya, implementasikan SavedStateProvider dan tetapkan sebagai penyedia pada SavedStateHandle untuk ViewModel:

private fun File.saveTempFile() = bundleOf("path", absolutePath)

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Untuk memulihkan data File saat pengguna kembali, ambil temp_file Bundle dari SavedStateHandle. Ini adalah Bundle yang sama yang disediakan oleh saveTempFile() berisi jalur absolut. Jalur absolut tersebut kemudian dapat digunakan untuk membuat instance File baru.

private fun File.saveTempFile() = bundleOf("path", absolutePath)

private fun Bundle.restoreTempFile() = if (containsKey("path")) {
    File(getString("path"))
} else {
    null
}

class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() {
    private var tempFile: File? = null
    init {
        val tempFileBundle = savedStateHandle.get<Bundle>("temp_file")
        if (tempFileBundle != null) {
            tempFile = tempFileBundle.restoreTempFile()
        }
        savedStateHandle.setSavedStateProvider("temp_file") { // saveState()
            if (tempFile != null) {
                tempFile.saveTempFile()
            } else {
                Bundle()
            }
        }
    }

    fun createOrGetTempFile(): File {
      return tempFile ?: File.createTempFile("temp", null).also {
          tempFile = it
      }
    }
}

SavedStateHandle dalam pengujian

Untuk menguji ViewModel yang menggunakan SavedStateHandle sebagai dependensi, buat instance SavedStateHandle baru dengan nilai pengujian yang diperlukan dan teruskan ke instance ViewModel yang sedang Anda uji.

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

Referensi lainnya

Guna mengetahui informasi modul Status Tersimpan untuk ViewModel lebih lanjut, baca referensi berikut.

Codelab

Melihat konten