Moduł Saved State dla ViewModel części Androida 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 obrotu ekranu
ani w innych sytuacjach. Jeśli jednak musisz obsługiwać zakończenie procesu zainicjowane przez system, możesz użyć interfejsu SavedStateHandle jako kopii zapasowej.
Stan interfejsu jest zwykle przechowywany w obiektach ViewModel lub się do nich odwołuje, więc używanie
rememberSaveable w Compose wymaga pewnego kodu, który może obsłużyć moduł
Saved State.
Gdy używasz tego modułu, ViewModel obiekty otrzymują obiekt SavedStateHandle
za pomocą konstruktora. Ten obiekt to mapa klucz-wartość, która umożliwia zapisywanie i pobieranie obiektów w stanie zapisanym. Te wartości są zachowywane po zakończeniu procesu przez system i pozostają dostępne w tym samym obiekcie.
Stan zapisany jest powiązany ze stosem zadań. Jeśli stos zadań zniknie, zniknie też stan zapisany. Może się to zdarzyć, gdy wymusisz zatrzymanie aplikacji, usuniesz ją z menu ostatnich aplikacji lub ponownie uruchomisz urządzenie. W takich przypadkach stos zadań znika i nie można przywrócić informacji w stanie zapisanym. W scenariuszach odrzucenia stanu interfejsu zainicjowanych przez użytkownika stan zapisany nie jest przywracany. W scenariuszach zainicjowanych przez system jest.
W przypadku stanu używanego w logice biznesowej przechowuj go w ViewModel i zapisuj za pomocąSavedStateHandle. W przypadku stanu używanego w
logice interfejsu użyj rememberSaveable w Compose.Konfiguracja
Aby używać SavedStateHandle, zaakceptuj go jako argument konstruktora w ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Następnie możesz pobrać instancję ViewModel w swoich elementach kompozycyjnych bez dodatkowej konfiguracji. Domyślna fabryka ViewModel udostępnia odpowiedni SavedStateHandle do ViewModel.
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
Podczas udostępniania niestandardowej instancji ViewModelProvider.Factory możesz
włączyć używanie SavedStateHandle, korzystając z CreationExtras i DSL viewModelFactory.
Praca z SavedStateHandle
Klasa SavedStateHandle to mapa klucz-wartość, która umożliwia zapisywanie i
pobieranie danych w stanie zapisanym 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 i po ponownym utworzeniu bez konieczności ręcznego zapisywania, przywracania i przekazywania tej wartości z powrotem do ViewModel przez aktywność lub fragment.
SavedStateHandle zapisuje tylko dane zapisane w nim, gdy
host Activity jest zatrzymany (np. gdy aplikacja jest wysyłana w tle).
Zapisywanie w SavedStateHandle podczas zatrzymania Activity nie jest zapisywane, chyba że Activity otrzyma onStart, a potem ponownie onStop (np. gdy aplikacja jest wysyłana na pierwszy plan, a potem ponownie w tle).SavedStateHandle ma też inne metody, których możesz się spodziewać podczas interakcji z mapą klucz-wartość:
contains(String key)– sprawdza, czy istnieje wartość dla danego klucza.remove(String key)– usuwa wartość dla danego klucza.keys()– zwraca wszystkie klucze zawarte wSavedStateHandle.
Dodatkowo możesz pobierać wartości z SavedStateHandle za pomocą obserwowalnego kontenera danych. Lista obsługiwanych typów obejmuje:
get() i set(). W Compose stan jest rejestrowany tylko wtedy, gdy aplikacja przechodzi w tle. Oznacza to, że chociaż możesz nadal aktualizować dane dostępne do obserwacji z SavedStateHandle, gdy aplikacja działa w tle, wszystkie aktualizacje stanu mogą zostać utracone, jeśli proces aplikacji zostanie zakończony, zanim aplikacja ponownie przejdzie na pierwszy plan.StateFlow
Możesz pobierać wartości z SavedStateHandle opakowane w obserwowalny StateFlow. W zależności od tego, czy musisz bezpośrednio zmieniać wartość, możesz wybrać strumień tylko do odczytu lub modyfikowalny:
getStateFlow(): użyj tej opcji, jeśli musisz 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 do odczytu i zapisu. Aktualizowanie.valuezwróconegoMutableStateFlowautomatycznie aktualizuje bazowySavedStateHandle, dzięki czemu nie musisz ręcznie ustawiać klucza.
Najczęściej aktualizujesz te wartości w wyniku interakcji użytkownika, np. wpisania zapytania w celu przefiltrowania listy danych.
getMutableStateFlow w SavedStateHandle została dodana w
cyklu życia w wersji 2.9.0.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 serializacją KotlinX. 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ć, oznacz klasę danych adnotacją @Serializable i użyj delegata saved w ViewModel:
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) } }
saved jest oceniany leniwie. Nie wywołuje lambdy inicjowania ani nie zapisuje niczego w SavedStateHandle, dopóki nie zostanie po raz pierwszy uzyskany dostęp do właściwości.Obsługa stanu Compose
Jeśli Twój stan zależy od interfejsów API Saver w Compose, a nie od serializacji KotlinX
, artefakt lifecycle-viewmodel-compose udostępnia delegata
saveable. Umożliwia to interoperacyjność między
SavedStateHandle a Saver w Compose, dzięki czemu każdy State, który można
zapisać za pomocą rememberSaveable z niestandardowym Saver, można też 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 } } }
Typy obsługiwane
Dane przechowywane w SavedStateHandle są zapisywane i przywracane jako Bundle,
wraz z resztą savedInstanceState aplikacji.
Typy obsługiwane bezpośrednio
Domyślnie możesz wywoływać set() i get() w SavedStateHandle dla tych samych typów danych co Bundle, jak pokazano poniżej:
| Obsługa typu/zajęć | 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 z powyższej listy, rozważ utworzenie klasy parcelable przez dodanie @Parcelize Kotlin annotation lub bezpośrednie zaimplementowanie Parcelable.
Zapisywanie klas nieparcelable
Jeśli klasa nie implementuje Parcelable ani Serializable i nie można jej zmodyfikować tak, aby implementowała jeden z tych interfejsów, nie można bezpośrednio zapisać instancji tej klasy w SavedStateHandle.
Od cyklu życia Lifecycle 2.3.0-alpha03 SavedStateHandle umożliwia zapisywanie
dowolnego obiektu przez udostępnienie własnej logiki zapisywania i przywracania obiektu jako
Bundle za pomocą metody setSavedStateProvider().
SavedStateRegistry.SavedStateProvider to interfejs, który definiuje
pojedynczą metodę saveState(), która zwraca Bundle zawierający stan
do zapisania. Gdy SavedStateHandle jest gotowy do zapisania swojego stanu, wywołuje saveState(), aby pobrać Bundle z SavedStateProvider, i zapisuje Bundle dla powiązanego klucza.
Rozważmy przykład aplikacji, która prosi o obraz z aplikacji aparatu za pomocą
intencji ACTION_IMAGE_CAPTURE, przekazując tymczasowy plik, w którym
aparat ma przechowywać 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 tymczasowy plik nie został utracony, jeśli proces aktywności zostanie zakończony i później przywrócony, TempFileViewModel może użyć SavedStateHandle do utrwalania swoich danych. Aby umożliwić TempFileViewModel zapisywanie danych, zaimplementuj
SavedStateProvider i ustaw go jako dostawcę w SavedStateHandle w
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 Bundle temp_file z SavedStateHandle. Jest to ten sam Bundle udostępniany przez saveTempFile(), który zawiera ścieżkę bezwzględną. Ścieżka bezwzględna może być następnie używana do utworzenia nowej instancji 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ć ViewModel, który przyjmuje SavedStateHandle jako zależność, utwórz nową instancję SavedStateHandle z wymaganymi wartościami testowymi i przekaż ją do testowanej instancji 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 Saved State dla ViewModel znajdziesz w tych materiałach.
Codelabs
Treści dotyczące widoków
Polecane dla Ciebie
- Uwaga: tekst linku jest wyświetlany, gdy język JavaScript jest wyłączony.
- Zapisywanie stanów interfejsu
- Praca z danymi dostępnymi do obserwacji
- Tworzenie ViewModel z zależnościami