Moduł Saved State dla ViewModel Jest częścią Android Jetpack.
Jak wspomnieliśmy w artykule Zapisywanie stanów interfejsu, obiekty ViewModel mogą obsługiwać zmiany konfiguracji, więc nie musisz się martwić o stan w przypadku obrotów ani innych sytuacji. Jeśli jednak musisz obsłużyć śmierć procesu zainicjowaną przez system, możesz użyć interfejsu API SavedStateHandle jako kopii zapasowej.
Stan interfejsu jest zwykle przechowywany lub przywoływany w obiektach ViewModel, więc użycie rememberSaveable w Compose wymaga pewnego stałego kodu, który może obsłużyć moduł zapisanego stanu.
Podczas korzystania z tego modułu obiekty ViewModel otrzymują obiekt SavedStateHandle za pomocą konstruktora. Ten obiekt to mapa klucz-wartość, która umożliwia zapisywanie i pobieranie obiektów do i z zapisanego stanu. Te wartości są zachowywane po zakończeniu procesu przez system i pozostają dostępne w tym samym obiekcie.
Zapisany stan jest powiązany ze stosem zadań. Jeśli stos zadań zniknie, zniknie też zapisany stan. Może się tak zdarzyć, gdy wymusisz zatrzymanie aplikacji, usuniesz ją z menu ostatnio używanych aplikacji lub ponownie uruchomisz urządzenie. W takich przypadkach stos zadań znika i nie można przywrócić informacji w zapisanym stanie. W przypadku zamknięcia stanu interfejsu przez użytkownika zapisany stan nie jest przywracany. W scenariuszach inicjowanych przez system jest to możliwe.
Konfiguracja
Aby użyć SavedStateHandle, zaakceptuj go jako argument konstruktora dla ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Następnie możesz pobrać instancję ViewModel w funkcjach kompozycyjnych bez dodatkowej konfiguracji. Domyślna fabryka ViewModel dostarcza odpowiedni obiekt SavedStateHandle do obiektu ViewModel.
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
Jeśli podajesz niestandardową instancję ViewModelProvider.Factory, możesz włączyć korzystanie z SavedStateHandle za pomocą CreationExtras i DSL viewModelFactory.
Praca z SavedStateHandle
Klasa SavedStateHandle to mapa klucz-wartość, która umożliwia zapisywanie i pobieranie danych do i z zapisanego stanu za pomocą metod set() i get().
Dzięki użyciu SavedStateHandle wartość zapytania jest zachowywana po śmierci procesu, co gwarantuje, że użytkownik zobaczy ten sam zestaw przefiltrowanych danych przed odtworzeniem i po nim, bez konieczności ręcznego zapisywania, przywracania i przekazywania tej wartości z powrotem do ViewModel przez aktywność lub fragment.
SavedStateHandle ma też inne metody, których można się spodziewać podczas interakcji z mapą klucz-wartość:
contains(String key)– sprawdza, czy dla danego klucza istnieje wartość.remove(String key)– usuwa wartość dla danego klucza.keys()– zwraca wszystkie klucze zawarte wSavedStateHandle.
Dodatkowo możesz pobierać wartości z SavedStateHandle za pomocą obiektu przechowującego dane dostępne do obserwacji. Lista obsługiwanych typów obejmuje:
StateFlow
Możesz pobierać wartości z SavedStateHandle opakowane w obserwowalny obiekt StateFlow. W zależności od tego, czy chcesz bezpośrednio zmienić wartość, możesz wybrać strumień tylko do odczytu lub strumień zmienny:
getStateFlow(): użyj tej opcji, jeśli chcesz tylko odczytać stan. Gdy zaktualizujesz wartość klucza w innym miejscu wSavedStateHandle, StateFlow otrzyma nową wartość. Jest to idealne rozwiązanie, gdy chcesz udostępnić strumień tylko do odczytu i przekształcić go za pomocą operatorów Flow.getMutableStateFlow(): użyj tej opcji, jeśli potrzebujesz dostępu zarówno do odczytu, jak i zapisu. Aktualizacja.valuezwróconegoMutableStateFlowautomatycznie aktualizuje bazowySavedStateHandle, dzięki czemu nie musisz ręcznie ustawiać klucza.
Najczęściej aktualizujesz te wartości w wyniku interakcji użytkownika, takich jak wpisanie zapytania w celu filtrowania listy danych.
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 } }
Obsługa serializacji KotlinX
W przypadku złożonego stanu interfejsu możesz użyć delegata właściwości saved wraz z KotlinX Serialization. Ten delegat umożliwia utrwalanie niestandardowych klas danych @Serializable bezpośrednio w SavedStateHandle. Dzięki temu stan ViewModel jest zachowywany po śmierci procesu, a interfejs Compose może bezproblemowo przywrócić swój stan po ponownym utworzeniu.
Aby go użyć, dodaj do klasy danych adnotację @Serializable i użyj w ViewModelu delegata saved:
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) } }
Obsługa stanu tworzenia wiadomości
Jeśli stan opiera się na interfejsach API Saver Compose, a nie na serializacji KotlinX, artefakt lifecycle-viewmodel-compose udostępnia obiekt delegowany saveable. Umożliwia to współdziałanie między SavedStateHandle i Saver w Compose, dzięki czemu każdy element State, który można zapisać za pomocą rememberSaveable z niestandardowym elementem Saver, można również zapisać za pomocą SavedStateHandle.
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
Obsługiwane typy
Dane przechowywane w SavedStateHandle są zapisywane i przywracane jako Bundle wraz z pozostałymi savedInstanceState aplikacji.
Typy obsługiwane bezpośrednio
Domyślnie możesz wywoływać metody set() i get() na obiekcie SavedStateHandle w przypadku tych samych typów danych co w przypadku obiektu Bundle, jak pokazano poniżej:
| Obsługa typów i klas | Obsługa tablic |
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+) |
Jeśli klasa nie rozszerza żadnej z klas na powyższej liście, rozważ dodanie do niej adnotacji @Parcelize w języku Kotlin lub bezpośrednie zaimplementowanie interfejsu Parcelable.
Zapisywanie klas, których nie można przekazać
Jeśli klasa nie implementuje interfejsu Parcelable ani Serializable i nie można jej zmodyfikować, aby implementowała jeden z tych interfejsów, nie można bezpośrednio zapisać instancji tej klasy w SavedStateHandle.
Począwszy od wersji Lifecycle 2.3.0-alpha03, SavedStateHandle umożliwia zapisanie dowolnego obiektu przez podanie własnej logiki zapisywania i przywracania obiektu jako Bundle za pomocą metody setSavedStateProvider().
SavedStateRegistry.SavedStateProvider to interfejs, który definiuje pojedynczą metodę saveState() zwracającą obiekt Bundle zawierający stan, który chcesz zapisać. Gdy SavedStateHandle jest gotowy do zapisania stanu, wywołuje saveState(), aby pobrać Bundle z SavedStateProvider, i zapisuje Bundle dla powiązanego klucza.
Rozważmy przykład aplikacji, która żąda obrazu z aplikacji aparatu za pomocą intencji ACTION_IMAGE_CAPTURE, przekazując plik tymczasowy, w którym aparat powinien zapisać obraz. TempFileViewModel zawiera logikę tworzenia tego pliku tymczasowego.
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Aby mieć pewność, że plik tymczasowy nie zostanie utracony, jeśli proces aktywności zostanie zakończony, a później przywrócony, TempFileViewModel może użyć SavedStateHandle do zachowania danych. Aby umożliwić usłudze TempFileViewModel zapisywanie danych, zaimplementuj SavedStateProvider i ustaw ją jako dostawcę w SavedStateHandle elementu 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 } } }
Aby przywrócić dane File, gdy użytkownik wróci, pobierz temp_file
Bundle z SavedStateHandle. Jest to ten sam parametr Bundle podany przez saveTempFile(), który zawiera ścieżkę bezwzględną. Ścieżki bezwzględnej można następnie użyć do utworzenia instancji nowego obiektu File.
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 w testach
Aby przetestować element ViewModel, który wymaga elementu SavedStateHandle, utwórz nową instancję elementu SavedStateHandle z wymaganymi wartościami testowymi i przekaż ją do testowanej instancji elementu ViewModel.
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
Dodatkowe materiały
Więcej informacji o module zapisanego stanu w ViewModel znajdziesz w tych materiałach.
Codelabs
Wyświetlanie treści
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Zapisywanie stanów interfejsu
- Praca z obiektami danych dostępnych do obserwacji
- Tworzenie obiektów ViewModel z zależnościami