UI-Zustände speichern

In diesem Leitfaden werden die Erwartungen der Nutzer an den UI-Status und die verfügbaren Optionen zum Beibehalten des Status erläutert.

Es ist wichtig, den UI-Status schnell zu speichern und wiederherzustellen, nachdem das System die Hostaktivität oder den Anwendungsprozess beendet hat. Nutzer erwarten, dass der UI-Status gleich bleibt. Das System kann jedoch die Activity, die den Bildschirm hostet, und den darin gespeicherten Status löschen.

Um die Erwartungen der Nutzer und das Systemverhalten in Einklang zu bringen, verwenden Sie eine Kombination der folgenden Methoden:

Die optimale Lösung hängt von der Komplexität der UI-Daten, den Anwendungsfällen Ihrer App und dem Gleichgewicht zwischen Datenzugriffsgeschwindigkeit und Arbeitsspeichernutzung ab.

Ihre App sollte die Erwartungen der Nutzer erfüllen und eine schnelle, reaktionsfähige Benutzeroberfläche bieten. Vermeiden Sie Verzögerungen beim Laden von Daten in die Benutzeroberfläche, insbesondere nach häufigen Konfigurationsänderungen wie dem Drehen des Geräts.

Erwartungen der Nutzer und Systemverhalten

Je nach Aktion des Nutzers erwarten sie, dass der UI-Status entweder gelöscht oder beibehalten wird. In einigen Fällen tut das System automatisch, was der Nutzer erwartet. In anderen Fällen tut das System das Gegenteil.

Vom Nutzer initiierte Löschung des UI-Status

Der Nutzer erwartet, dass der temporäre UI-Status eines Bildschirms gleich bleibt, bis er ihn vollständig schließt. Der Nutzer kann einen Bildschirm oder eine App vollständig schließen, indem er Folgendes tut:

  • Die App vom Bildschirm „Übersicht“ (Zuletzt verwendet) wischen
  • Die App über den Bildschirm „Einstellungen“ beenden oder zwangsweise beenden
  • Das Gerät neu starten
  • Eine Art „Abschlussaktion“ ausführen (die von Activity.finish() unterstützt wird)

In diesen Fällen geht der Nutzer davon aus, dass er den Bildschirm dauerhaft verlassen hat. Wenn er zurückkehrt, erwartet er, dass der Bildschirm in einem sauberen Zustand startet. Das zugrunde liegende Systemverhalten für diese Fälle entspricht den Erwartungen des Nutzers: Die Hostaktivitätsinstanz wird beendet und aus dem Arbeitsspeicher entfernt, zusammen mit allen darin gespeicherten Status und allen zugehörigen gespeicherten Statusdatensätzen.

Es gibt einige Ausnahmen von dieser Regel zum vollständigen Schließen. Beispielsweise erwartet ein Nutzer möglicherweise, dass er in einem Browser genau zu der Webseite weitergeleitet wird, die er sich angesehen hat, bevor er den Browser mit dem Button „Zurück“ verlassen hat.

Vom System initiierte Löschung des UI-Status

Ein Nutzer erwartet, dass der UI-Status eines Bildschirms bei einer Konfigurationsänderung wie dem Drehen des Geräts oder dem Wechsel in den Multifenstermodus gleich bleibt. Standardmäßig beendet das System jedoch die Hostaktivität, wenn eine solche Konfigurationsänderung auftritt, und löscht alle darin gespeicherten UI-Status. Weitere Informationen zu Gerätekonfigurationen finden Sie unter Auf Konfigurationsänderungen in Jetpack Compose reagieren.

Hinweis: Es ist möglich (aber nicht empfehlenswert), das Standardverhalten für Konfigurationsänderungen zu überschreiben. Weitere Informationen finden Sie unter Umgang mit Konfigurationsänderungen.

Ein Nutzer erwartet auch, dass der UI-Status Ihrer App gleich bleibt, wenn er vorübergehend zu einer anderen App wechselt und später zu Ihrer App zurückkehrt. Beispiel: Der Nutzer führt eine Suche auf einem Bildschirm aus und drückt dann die Home-Taste oder nimmt einen Anruf entgegen. Wenn er zum Suchbildschirm zurückkehrt, erwartet er, dass das Suchkeyword und die Ergebnisse noch genau wie zuvor vorhanden sind.

In diesem Fall wird Ihre App in den Hintergrund versetzt und das System versucht, den App-Prozess im Arbeitsspeicher zu behalten. Das System kann jedoch den Anwendungsprozess beenden, während der Nutzer mit anderen Apps interagiert. In diesem Fall wird die Hostaktivität zusammen mit allen darin gespeicherten Status beendet. Wenn der Nutzer die App neu startet, befindet sich der Bildschirm unerwartet in einem sauberen Zustand. Weitere Informationen zur Prozessbeendigung finden Sie unter Prozesse und App-Lebenszyklus.

Optionen zum Beibehalten des UI-Status

Wenn die Erwartungen des Nutzers an den UI-Status nicht mit dem Standardverhalten des Systems übereinstimmen, müssen Sie den UI-Status des Nutzers speichern und wiederherstellen, damit die vom System initiierte Beendigung für den Nutzer transparent ist.

Die einzelnen Optionen zum Beibehalten des UI-Status unterscheiden sich in den folgenden Dimensionen, die sich auf die Nutzererfahrung auswirken:

ViewModel Gespeicherter Status Nichtflüchtiger Speicher
Speicherort im Arbeitsspeicher im Arbeitsspeicher auf der Festplatte oder im Netzwerk
Übersteht Konfigurationsänderungen Ja Ja Ja
Übersteht vom System initiierte Prozessbeendigung Nein Ja Ja
Übersteht das vollständige Schließen des Bildschirms/finish() durch den Nutzer Nein Nein Ja
Datenbeschränkungen Komplexe Objekte sind in Ordnung, aber der Speicherplatz ist durch den verfügbaren Arbeitsspeicher begrenzt. Nur für primitive Typen und einfache, kleine Objekte wie String Nur durch Festplattenspeicher oder Kosten / Zeit für den Abruf aus der Netzwerkressource begrenzt
Lese-/Schreibzeit Schnell (nur Arbeitsspeicherzugriff) Langsam (erfordert Serialisierung/Deserialisierung) Langsam (erfordert Festplattenzugriff oder Netzwerktransaktion)

ViewModel zum Verarbeiten von Konfigurationsänderungen verwenden

ViewModel ist ideal zum Speichern und Verwalten von UI-bezogenen Daten, während der Nutzer die Anwendung aktiv verwendet. Es ermöglicht einen schnellen Zugriff auf UI-Daten und hilft Ihnen, Daten nicht immer wieder aus dem Netzwerk oder von der Festplatte abzurufen, wenn das Gerät gedreht wird, die Fenstergröße geändert wird oder andere häufig auftretende Konfigurationsänderungen auftreten. Informationen zum Implementieren eines ViewModel finden Sie im ViewModel-Leitfaden.

ViewModel behält die Daten im Arbeitsspeicher. Daher ist der Abruf günstiger als bei Daten von der Festplatte oder aus dem Netzwerk. Ein ViewModel ist mit einem Lebenszyklusinhaber verknüpft, z. B. einem Navigationsziel oder einer Aktivität. Es bleibt bei einer Konfigurationsänderung im Arbeitsspeicher und das System verknüpft das ViewModel automatisch mit der neuen Lebenszyklusinhaberinstanz, die sich aus der Konfigurationsänderung ergibt.

Im Gegensatz zum gespeicherten Status werden ViewModels bei einer vom System initiierten Prozessbeendigung beendet. Verwenden Sie die SavedStateHandle API, um Daten nach einer vom System initiierten Prozessbeendigung in einem ViewModel neu zu laden. Wenn die Daten mit der Benutzeroberfläche verknüpft sind und nicht im ViewModel gespeichert werden müssen, verwenden Sie stattdessen rememberSerializable. Verwenden Sie für primitive Datentypen oder Szenarien, in denen Sie @Serializable nicht verwenden möchten, rememberSaveable. Wenn es sich bei den Daten um Anwendungsdaten handelt, ist es möglicherweise besser, sie auf dem Laufwerk zu speichern.

Wenn Sie bereits eine In-Memory-Lösung zum Speichern des UI-Status bei Konfigurationsänderungen haben, müssen Sie ViewModel möglicherweise nicht verwenden.

Gespeicherten Status als Backup verwenden, um vom System initiierte Prozessbeendigungen zu verarbeiten

APIs wie rememberSerializable und rememberSaveable in Compose und SavedStateHandle in ViewModels speichern die Daten, die zum Neuladen des UI Status erforderlich sind, wenn das System eine Komponente beendet und später neu erstellt. Um komplexe Datenstrukturen effizienter zu verarbeiten, unterstützt SavedStateHandle die Kotlinx-Serialisierung über die saved {}-Erweiterung. So können Sie typsichere Objekte nahtlos zusammen mit standardmäßigen primitiven Typen beibehalten und wiederherstellen. Informationen zum Implementieren des gespeicherten Status mit rememberSaveable finden Sie unter Status und Jetpack Compose.

Gespeicherte Status-Bundles bleiben sowohl bei Konfigurationsänderungen als auch bei Prozessbeendigungen erhalten, sind jedoch durch Speicher und Geschwindigkeit begrenzt, da die verschiedenen APIs Daten serialisieren. Die Serialisierung kann viel Arbeitsspeicher verbrauchen, wenn die serialisierten Objekte komplex sind. Da dieser Vorgang während einer Konfigurationsänderung im Hauptthread erfolgt, kann eine lange Serialisierung zu ausgelassenen Frames und visuellen Rucklern führen.

Verwenden Sie den gespeicherten Status nicht, um große Datenmengen wie Bitmaps oder komplexe Datenstrukturen zu speichern, die eine lange Serialisierung oder Deserialisierung erfordern. Speichern Sie stattdessen nur primitive Typen und einfache, kleine Objekte wie String. Verwenden Sie den gespeicherten Status also, um eine minimale Menge an Daten zu speichern, z. B. eine ID, um die Daten neu zu erstellen, die erforderlich sind, um die Benutzeroberfläche in ihren vorherigen Zustand zurückzuversetzen, falls die anderen Persistenzmechanismen fehlschlagen. Die meisten Apps sollten dies implementieren, um vom System initiierte Prozessbeendigungen zu verarbeiten.

Je nach Anwendungsfällen Ihrer App müssen Sie den gespeicherten Status möglicherweise gar nicht verwenden. Ein Browser kann den Nutzer beispielsweise genau zu der Webseite zurückbringen, die er sich angesehen hat, bevor er den Browser verlassen hat. Wenn sich Ihre Aktivität so verhält, können Sie auf den gespeicherten Status verzichten und stattdessen alles lokal beibehalten.

Wenn Sie außerdem eine Aktivität über eine Intent öffnen, wird das Bundle mit Extras sowohl bei Konfigurationsänderungen als auch bei der Wiederherstellung der Aktivität durch das System an die Aktivität gesendet. Wenn ein Teil der UI-Statusdaten, z. B. eine Suchanfrage, beim Starten der Aktivität als Intent-Extra übergeben wurde, können Sie das Extras-Bundle anstelle des gespeicherten Status-Bundles verwenden. Weitere Informationen zu Intent-Extras finden Sie unter Intent und Intent-Filter.

In beiden Szenarien sollten Sie trotzdem ein ViewModel verwenden, um zu vermeiden, dass bei einer Konfigurationsänderung Zyklen für das Neuladen von Daten aus der Datenbank verschwendet werden.

In Fällen, in denen die beizubehaltenden UI-Daten einfach und klein sind, können Sie die APIs für den gespeicherten Status allein verwenden, um Ihre Statusdaten beizubehalten.

Mit SavedStateRegistry in den gespeicherten Status einbinden

Ab Fragment 1.1.0 oder der transitiven Abhängigkeit Activity 1.0.0 implementieren UI-Komponenten wie ComponentActivity SavedStateRegistryOwner und stellen eine SavedStateRegistry bereit, die an diese Komponente gebunden ist. Mit SavedStateRegistry können Komponenten in den gespeicherten Status eingebunden werden, um ihn zu verwenden oder dazu beizutragen. Beispielsweise verwendet das Modul „Gespeicherter Status“ für ViewModel SavedStateRegistry, um ein SavedStateHandle zu erstellen und es Ihren ViewModel-Objekten zur Verfügung zu stellen. Sie können die SavedStateRegistry in Ihrem Lebenszyklusinhaber abrufen, indem Sie savedStateRegistry aufrufen.

Komponenten, die zum gespeicherten Status beitragen, müssen SavedStateRegistry.SavedStateProvider implementieren, das eine einzelne Methode namens saveState() definiert. Mit der Methode saveState() kann Ihre Komponente ein Bundle zurückgeben, das alle Status enthält, die aus dieser Komponente gespeichert werden sollen. SavedStateRegistry ruft diese Methode während der Phase zum Speichern des Status im Lebenszyklus des Lebenszyklusinhabers auf.

  class SearchManager : SavedStateRegistry.SavedStateProvider {
      companion object {
          private const val QUERY = "query"
      }

      private val query: String? = null

      ...

      override fun saveState(): Bundle {
          return bundleOf(QUERY to query)
      }
  }

Rufen Sie registerSavedStateProvider() in der SavedStateRegistry auf, um einen SavedStateProvider zu registrieren. Übergeben Sie dabei einen Schlüssel, der mit den Daten des Anbieters verknüpft werden soll, sowie den Anbieter. Die zuvor gespeicherten Daten für den Anbieter können aus dem gespeicherten Status abgerufen werden, indem Sie consumeRestoredStateForKey() in der SavedStateRegistry aufrufen und den Schlüssel übergeben, der mit den Daten des Anbieters verknüpft ist.

In einer ComponentActivity können Sie einen SavedStateProvider in onCreate() registrieren, nachdem Sie super.onCreate() aufgerufen haben. Alternativ können Sie einen LifecycleObserver für einen SavedStateRegistryOwner festlegen, der LifecycleOwner implementiert, und den SavedStateProvider registrieren, sobald das Ereignis ON_CREATE eintritt. Mit einem LifecycleObserver können Sie die Registrierung und den Abruf des zuvor gespeicherten Status von der SavedStateRegistryOwner selbst entkoppeln.

  class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
      companion object {
          private const val PROVIDER = "search_manager"
          private const val QUERY = "query"
      }

      private val query: String? = null

      init {
          // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
          registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
              if (event == Lifecycle.Event.ON_CREATE) {
                  val registry = registryOwner.savedStateRegistry

                  // Register this object for future calls to saveState()
                  registry.registerSavedStateProvider(PROVIDER, this)

                  // Get the previously saved state and restore it
                  val state = registry.consumeRestoredStateForKey(PROVIDER)

                  // Apply the previously saved state
                  query = state?.getString(QUERY)
              }
          }
      }

      override fun saveState(): Bundle {
          return bundleOf(QUERY to query)
      }

      ...
  }

  class SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

Lokale Persistenz verwenden, um Prozessbeendigungen für komplexe oder große Daten zu verarbeiten

Nichtflüchtiger lokaler Speicher wie eine Datenbank oder DataStore bleibt so lange erhalten, wie Ihre Anwendung auf dem Gerät des Nutzers installiert ist (es sei denn, der Nutzer löscht die Daten für Ihre App). Ein solcher lokaler Speicher übersteht zwar vom System initiierte Beendigungen des Anwendungsprozesses, der Abruf kann jedoch teuer sein, da er aus dem lokalen Speicher in den Arbeitsspeicher gelesen werden muss. Oft ist dieser nichtflüchtige lokale Speicher bereits Teil Ihrer Anwendungsarchitektur, um alle Daten zu speichern, die Sie nicht verlieren möchten, wenn Sie die App öffnen und schließen.

Weder ViewModel noch Status, der mit rememberSerializable, rememberSaveable oder SavedStateHandle gespeichert wurde, sind langfristige Speicherlösungen und daher kein Ersatz für lokalen Speicher wie eine Datenbank. Verwenden Sie diese Mechanismen stattdessen nur zum vorübergehenden Speichern des temporären UI-Status und verwenden Sie nichtflüchtigen Speicher für andere App-Daten. Weitere Informationen zur Nutzung des lokalen Speichers, um Ihre App-Modelldaten langfristig beizubehalten (z.B. bei Neustarts des Geräts), finden Sie im Leitfaden zur App-Architektur.

UI-Status verwalten: Teile und herrsche

Sie können den UI-Status effizient speichern und wiederherstellen, indem Sie die Arbeit auf die verschiedenen Arten von Persistenzmechanismen aufteilen. In den meisten Fällen sollte jeder dieser Mechanismen einen anderen Datentyp speichern, der in der App verwendet wird, basierend auf den Kompromissen zwischen Datenkomplexität, Zugriffsgeschwindigkeit und Lebensdauer:

  • Lokale Persistenz: Speichert alle Anwendungsdaten, die Sie nicht verlieren möchten, wenn Sie die App öffnen und schließen.
    • Beispiel: Eine Sammlung von Song-Objekten, die Audiodateien und Metadaten enthalten können.
  • ViewModel: Speichert im Arbeitsspeicher alle Daten, die zum Anzeigen der zugehörigen Benutzeroberfläche erforderlich sind, den UI-Status des Bildschirms.
    • Beispiel: Die Song-Objekte der letzten Suche und die letzte Suchanfrage.
  • Gespeicherter Status (rememberSerializable, rememberSaveable und SavedStateHandle): Speichert eine kleine Menge an Daten, die zum Neuladen des UI-Status erforderlich sind, wenn das System die Benutzeroberfläche beendet und dann neu erstellt. Anstatt hier komplexe Objekte zu speichern, sollten Sie die komplexen Objekte im lokalen Speicher beibehalten und eine eindeutige ID für diese Objekte in den APIs für den gespeicherten Status speichern.
    • Beispiel: Die letzte Suchanfrage speichern.

Nehmen wir als Beispiel eine App, mit der Sie Ihre Song-Bibliothek durchsuchen können. So sollten verschiedene Ereignisse verarbeitet werden:

Wenn der Nutzer einen Song hinzufügt, delegiert das ViewModel sofort die lokale Persistenz dieser Daten. Wenn dieser neu hinzugefügte Song in der Benutzeroberfläche angezeigt werden soll, müssen Sie auch die Daten im ViewModel-Objekt aktualisieren, um das Hinzufügen des Songs widerzuspiegeln. Alle Datenbankeinfügungen müssen außerhalb des Hauptthreads erfolgen.

Wenn der Nutzer nach einem Song sucht, sollten alle komplexen Song-Daten, die Sie aus der Datenbank laden, sofort im ViewModel-Objekt als Teil des UI-Status des Bildschirms gespeichert werden.

Wenn die App in den Hintergrund versetzt wird und das System den Status speichert, sollte die Suchanfrage mit APIs für den gespeicherten Status gespeichert werden, falls der Prozess neu erstellt wird. Da die Informationen erforderlich sind, um Anwendungsdaten zu laden, die hier beibehalten werden, speichern Sie die Suchanfrage im ViewModel SavedStateHandle oder verwenden Sie rememberSerializable oder rememberSaveable in Ihren Composables. Das sind alle Informationen, die Sie benötigen, um die Daten zu laden und die Benutzeroberfläche wieder in den aktuellen Zustand zu versetzen.

Komplexe Status wiederherstellen: Teile zusammensetzen

Wenn der Nutzer zur App zurückkehrt, gibt es zwei mögliche Szenarien für die Neuerstellung der Benutzeroberfläche:

  • Die Benutzeroberfläche wird neu erstellt, nachdem das System den Anwendungsprozess beendet hat. Die Abfrage wurde vom System mit APIs für den gespeicherten Status gespeichert. Das ViewModel (mit SavedStateHandle) oder das Composable (mit rememberSerializable oder rememberSaveable) stellt die Abfrage automatisch wieder her. Wenn das Composable die Abfrage wiederherstellt, übergibt es sie an das ViewModel. Das ViewModel stellt fest, dass keine Suchergebnisse im Cache gespeichert sind, und delegiert das Laden der Suchergebnisse mit der angegebenen Suchanfrage.
  • Die Benutzeroberfläche wird nach einer Konfigurationsänderung neu erstellt. Da die ViewModel-Instanz nicht beendet wurde, sind alle Informationen im Cache des ViewModel gespeichert und die Datenbank muss nicht noch einmal abgefragt werden.

Zusätzliche Ressourcen

Weitere Informationen zum Speichern von UI-Status finden Sie in den folgenden Ressourcen.

Codelabs

Ansichteninhalte