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ść:

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 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 zarówno do odczytu, jak i zapisu. Aktualizacja .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, 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