Modul „Saved State“ für ViewModel   Teil von Android Jetpack.

Wie unter UI-Zustände speichern beschrieben, können ViewModel-Objekte Konfigurationsänderungen verarbeiten, sodass Sie sich keine Gedanken über den Zustand bei Rotationen oder anderen Fällen machen müssen. Wenn Sie jedoch den vom System initiierten Prozessabbruch verarbeiten müssen, sollten Sie die SavedStateHandle API als Backup verwenden.

Der UI-Status wird normalerweise in ViewModel-Objekten gespeichert oder darauf verwiesen. Die Verwendung von rememberSaveable in Compose erfordert daher etwas Boilerplate-Code, den das Modul für den gespeicherten Status für Sie übernehmen kann.

Wenn Sie dieses Modul verwenden, erhalten ViewModel-Objekte über ihren Konstruktor ein SavedStateHandle-Objekt. Dieses Objekt ist eine Schlüssel/Wert-Zuordnung, mit der Sie Objekte in den gespeicherten Status schreiben und daraus abrufen können. Diese Werte bleiben erhalten, nachdem der Prozess vom System beendet wurde, und sind weiterhin über dasselbe Objekt verfügbar.

Der gespeicherte Status ist an Ihren Aufgabenstapel gebunden. Wenn Ihr Aufgabenstapel verschwindet, geht auch der gespeicherte Status verloren. Das kann passieren, wenn Sie eine App erzwingen, sie aus dem Menü „Letzte Apps“ entfernen oder das Gerät neu starten. In solchen Fällen verschwindet der Task-Stack und Sie können die Informationen im gespeicherten Zustand nicht wiederherstellen. In Szenarien mit vom Nutzer initiiertem Schließen des UI-Zustands wird der gespeicherte Zustand nicht wiederhergestellt. In vom System initiierten Szenarien ist das der Fall.

Einrichtung

Wenn Sie SavedStateHandle verwenden möchten, akzeptieren Sie es als Konstruktorargument für ViewModel.

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

Anschließend können Sie eine Instanz von ViewModel in Ihren Composables ohne zusätzliche Konfiguration abrufen. Die Standard-ViewModel-Factory stellt die passende SavedStateHandle für Ihre ViewModel bereit.

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

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

Wenn Sie eine benutzerdefinierte ViewModelProvider.Factory-Instanz bereitstellen, können Sie die Verwendung von SavedStateHandle mit CreationExtras und der viewModelFactory-DSL aktivieren.

Mit SavedStateHandle arbeiten

Die Klasse SavedStateHandle ist eine Schlüssel/Wert-Zuordnung, mit der Sie über die Methoden set() und get() Daten in den gespeicherten Zustand schreiben und daraus abrufen können.

Wenn Sie SavedStateHandle verwenden, bleibt der Abfragewert auch nach dem Beenden des Prozesses erhalten. So sehen Nutzer vor und nach dem erneuten Erstellen dieselben gefilterten Daten, ohne dass die Aktivität oder das Fragment den Wert manuell speichern, wiederherstellen und an ViewModel weiterleiten muss.

SavedStateHandle hat auch andere Methoden, die Sie bei der Interaktion mit einer Schlüssel/Wert-Zuordnung erwarten würden:

  • contains(String key): Prüft, ob ein Wert für den angegebenen Schlüssel vorhanden ist.
  • remove(String key): Entfernt den Wert für den angegebenen Schlüssel.
  • keys(): Gibt alle Schlüssel zurück, die in SavedStateHandle enthalten sind.

Außerdem können Sie Werte aus SavedStateHandle mit einem beobachtbaren Daten-Holder abrufen. Die Liste der unterstützten Typen umfasst Folgendes:

StateFlow

Sie können Werte aus SavedStateHandle abrufen, die in einem StateFlow-Observable umschlossen sind. Je nachdem, ob Sie den Wert direkt ändern müssen, können Sie zwischen einem schreibgeschützten oder einem veränderbaren Stream wählen:

  • getStateFlow(): Verwenden Sie diese Option, wenn Sie nur den Status lesen müssen. Wenn Sie den Wert des Schlüssels an anderer Stelle in der SavedStateHandle aktualisieren, empfängt StateFlow den neuen Wert. Das ist ideal, wenn Sie einen schreibgeschützten Stream bereitstellen und ihn mit Flow-Operatoren transformieren möchten.
  • getMutableStateFlow(): Verwenden Sie diese Option, wenn Sie sowohl Lese- als auch Schreibzugriff benötigen. Wenn Sie die .value des zurückgegebenen MutableStateFlow aktualisieren, wird die zugrunde liegende SavedStateHandle automatisch aktualisiert. Sie müssen den Schlüssel also nicht manuell festlegen.

In den meisten Fällen aktualisieren Sie diese Werte aufgrund von Nutzerinteraktionen, z. B. durch Eingabe einer Suchanfrage zum Filtern einer Liste von Daten.

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

Unterstützung für KotlinX Serialization

Für komplexen UI-Status können Sie den saved-Property-Delegate zusammen mit KotlinX Serialization verwenden. Mit diesem Delegaten können Sie benutzerdefinierte @Serializable-Datenklassen direkt in SavedStateHandle speichern. So bleibt der Status Ihres ViewModels auch nach dem Beenden des Prozesses erhalten und Ihre Compose-UI kann ihren Status bei der Neuerstellung nahtlos wiederherstellen.

Dazu müssen Sie Ihre Datenklasse mit @Serializable annotieren und das saved-Delegate in Ihrem ViewModel verwenden:

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

Compose State-Unterstützung

Wenn Ihr Status auf den Saver-APIs von Compose anstelle von KotlinX Serialization basiert, stellt das lifecycle-viewmodel-compose-Artefakt den Delegaten saveable bereit. Dadurch wird die Interoperabilität zwischen SavedStateHandle und Saver von Compose ermöglicht. Alle State, die Sie über rememberSaveable mit einem benutzerdefinierten Saver speichern können, können auch mit SavedStateHandle gespeichert werden.

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

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

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

Unterstützte Typen

Daten, die in einem SavedStateHandle gespeichert sind, werden als Bundle zusammen mit den restlichen savedInstanceState für Ihre App gespeichert und wiederhergestellt.

Direkt unterstützte Typen

Standardmäßig können Sie set() und get() für ein SavedStateHandle für dieselben Datentypen wie für ein Bundle aufrufen, wie unten gezeigt:

Unterstützung für Typen/Klassen Unterstützung für Arrays
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+)

Wenn die Klasse nicht von einer der oben aufgeführten Klassen abgeleitet wird, sollten Sie die Klasse durch Hinzufügen der Kotlin-Annotation @Parcelize oder durch direkte Implementierung von Parcelable in ein Parcelable umwandeln.

Nicht serialisierbare Klassen speichern

Wenn eine Klasse Parcelable oder Serializable nicht implementiert und nicht so geändert werden kann, dass eines dieser Interfaces implementiert wird, ist es nicht möglich, eine Instanz dieser Klasse direkt in einem SavedStateHandle zu speichern.

Ab Lifecycle 2.3.0-alpha03 können Sie mit SavedStateHandle jedes Objekt speichern, indem Sie Ihre eigene Logik zum Speichern und Wiederherstellen des Objekts als Bundle mit der Methode setSavedStateProvider() bereitstellen. SavedStateRegistry.SavedStateProvider ist eine Schnittstelle, die eine einzelne saveState()-Methode definiert, die ein Bundle mit dem Status zurückgibt, den Sie speichern möchten. Wenn SavedStateHandle bereit ist, seinen Status zu speichern, ruft es saveState() auf, um Bundle aus SavedStateProvider abzurufen, und speichert Bundle für den zugehörigen Schlüssel.

Nehmen wir als Beispiel eine App, die über den Intent ACTION_IMAGE_CAPTURE ein Bild von der Kamera-App anfordert und eine temporäre Datei übergibt, in der die Kamera das Bild speichern soll. Die Logik zum Erstellen dieser temporären Datei ist in TempFileViewModel enthalten.

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

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

Damit die temporäre Datei nicht verloren geht, wenn der Prozess der Aktivität beendet und später wiederhergestellt wird, kann TempFileViewModel die SavedStateHandle verwenden, um ihre Daten beizubehalten. Damit TempFileViewModel seine Daten speichern kann, implementieren Sie SavedStateProvider und legen Sie sie als Anbieter für SavedStateHandle von ViewModel fest:

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

Wenn der Nutzer zurückkehrt, rufen Sie die temp_file Bundle aus dem SavedStateHandle ab, um die File-Daten wiederherzustellen. Dies ist dieselbe Bundle, die von saveTempFile() bereitgestellt wird und den absoluten Pfad enthält. Der absolute Pfad kann dann verwendet werden, um eine neue File zu instanziieren.

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 in Tests

Wenn Sie ein ViewModel testen möchten, das ein SavedStateHandle als Abhängigkeit verwendet, erstellen Sie eine neue Instanz von SavedStateHandle mit den erforderlichen Testwerten und übergeben Sie sie an die ViewModel-Instanz, die Sie testen.

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

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

Zusätzliche Ressourcen

Weitere Informationen zum Modul „Gespeicherter Status“ für ViewModel finden Sie in den folgenden Ressourcen.

Codelabs

Inhalte ansehen