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.

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 ma też inne metody, których możesz się spodziewać podczas interakcji z mapą klucz-wartość:

Dodatkowo możesz pobierać wartości z SavedStateHandle za pomocą obserwowalnego kontenera danych. Lista obsługiwanych typów obejmuje:

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 w SavedStateHandle, 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 .value zwróconego MutableStateFlow automatycznie aktualizuje bazowy SavedStateHandle, 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.

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)
    }
}

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