UI-Zustände (Ansichten) speichern

Konzepte und Jetpack Compose-Implementierung

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

Das schnelle Speichern und Wiederherstellen des UI-Zustands einer Aktivität, nachdem das System Aktivitäten oder Anwendungen beendet hat, ist für eine gute Nutzererfahrung unerlässlich. Nutzer erwarten, dass der UI-Status gleich bleibt, aber das System kann die Activity und ihren gespeicherten Status löschen.

Um die Lücke zwischen Nutzererwartungen und Systemverhalten zu schließen, können Sie eine Kombination der folgenden Methoden verwenden:

  • ViewModel-Objekte.
  • Gespeicherte Instanzstatus in den folgenden Kontexten:
  • Lokaler Speicher zum Beibehalten des UI-Status während App- und Aktivitätsübergängen.

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

Ihre App sollte den Erwartungen der Nutzer entsprechen 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 der Rotation.

Nutzererwartungen und Systemverhalten

Je nach Aktion des Nutzers wird erwartet, dass der Aktivitätsstatus entweder gelöscht oder beibehalten wird. In einigen Fällen führt das System automatisch die erwartete Aktion aus. In anderen Fällen macht das System das Gegenteil.

Vom Nutzer initiierte Schließung des UI-Status

Der Nutzer erwartet, dass der vorübergehende UI-Status einer Aktivität gleich bleibt, bis er die Aktivität vollständig schließt. Der Nutzer kann eine Aktivität vollständig schließen, indem er Folgendes tut:

  • Wenn Sie die Aktivität vom Bildschirm „Übersicht“ (Letzte) wischen.
  • Die App wird über die Einstellungen beendet oder das Beenden der App wird erzwungen.
  • Gerät neu starten
  • Abschluss einer Art „Abschluss“-Aktion (die von Activity.finish() unterstützt wird).

In diesen Fällen geht der Nutzer davon aus, dass er die Aktivität dauerhaft verlassen hat. Wenn er die Aktivität wieder öffnet, erwartet er, dass sie von Anfang an beginnt. Das zugrunde liegende Systemverhalten für diese Schließungsszenarien entspricht den Erwartungen der Nutzer: Die Aktivitätsinstanz wird zerstört und aus dem Arbeitsspeicher entfernt, zusammen mit allen darin gespeicherten Status und allen gespeicherten Instanzstatusdatensätzen, die mit der Aktivität verknüpft sind.

Es gibt einige Ausnahmen von dieser Regel zum vollständigen Schließen. Ein Nutzer erwartet beispielsweise möglicherweise, dass er in einem Browser genau zu der Webseite zurückkehrt, die er sich angesehen hat, bevor er den Browser über den Button „Zurück“ geschlossen hat.

Vom System initiierte Schließung des UI-Status

Nutzer erwarten, dass der UI-Zustand einer Aktivität während einer Konfigurationsänderung, z. B. einer Drehung oder dem Wechsel in den Mehrfenstermodus, gleich bleibt. Standardmäßig wird die Aktivität jedoch vom System zerstört, wenn eine solche Konfigurationsänderung eintritt. Dadurch wird der in der Aktivitätsinstanz gespeicherte UI-Status gelöscht. Weitere Informationen zu Gerätekonfigurationen finden Sie auf der Referenzseite zur Konfiguration.

Das Standardverhalten für Konfigurationsänderungen kann überschrieben werden. Das wird jedoch nicht empfohlen. Weitere Informationen finden Sie unter Konfigurationsänderung verarbeiten.

Nutzer erwarten außerdem, dass der UI-Status Ihrer Aktivität gleich bleibt, wenn sie vorübergehend zu einer anderen App wechseln und später zu Ihrer App zurückkehren. Wenn der Nutzer beispielsweise eine Suche in Ihrer Suchaktivität durchführt und dann die Home-Schaltfläche drückt oder einen Anruf entgegennimmt, erwartet er, dass das Suchkeyword und die Ergebnisse bei der Rückkehr zur Suchaktivität noch genau wie zuvor vorhanden sind.

In diesem Szenario wird Ihre App in den Hintergrund verschoben und das System versucht, den App-Prozess im Arbeitsspeicher zu behalten. Das System kann den App-Prozess jedoch beenden, während der Nutzer mit anderen Apps interagiert. In diesem Fall wird die Aktivitätsinstanz zusammen mit dem darin gespeicherten Status zerstört. Wenn der Nutzer die App neu startet, befindet sich die Aktivität unerwartet in einem sauberen Zustand. Weitere Informationen zum Beenden von Prozessen finden Sie unter Prozesse und App-Lebenszyklus.

Optionen zum Beibehalten des UI-Zustands

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

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

ViewModel

Gespeicherter Instanzstatus

Nichtflüchtiger Speicher

Storage-Speicherort

im Arbeitsspeicher

im Arbeitsspeicher

auf der Festplatte oder im Netzwerk

Überlebt Konfigurationsänderungen

Ja

Ja

Ja

Überlebt die vom System initiierte Prozessbeendigung

Nein

Ja

Ja

Überlebt das Schließen der Aktivität durch den Nutzer/finish()

Nein

Nein

Ja

Dateneinschrä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 Speicherplatz oder Kosten / Zeit für den Abruf von 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. Sie ermöglicht den schnellen Zugriff auf UI-Daten und hilft Ihnen, Daten bei Änderungen der Konfiguration wie Drehen des Geräts oder Ändern der Fenstergröße nicht noch einmal aus dem Netzwerk oder von der Festplatte abzurufen. Informationen zur Implementierung eines ViewModels finden Sie im ViewModel-Leitfaden.

ViewModel behält die Daten im Arbeitsspeicher bei. Das Abrufen ist daher günstiger als das Abrufen von Daten von der Festplatte oder aus dem Netzwerk. Ein ViewModel ist einer Aktivität (oder einem anderen LifecycleOwner) zugeordnet. Es bleibt bei einer Konfigurationsänderung im Arbeitsspeicher und das System ordnet das ViewModel automatisch der neuen Aktivitätsinstanz zu, die sich aus der Konfigurationsänderung ergibt.

ViewModels werden automatisch vom System zerstört, wenn der Nutzer Ihre Aktivität oder Ihr Fragment verlässt oder wenn Sie finish() aufrufen. Das bedeutet, dass der Status in diesen Szenarien wie vom Nutzer erwartet gelöscht wird.

Im Gegensatz zum gespeicherten Instanzstatus werden ViewModels bei einem vom System initiierten Prozessende zerstört. Wenn Sie Daten nach einem vom System initiierten Prozessende in einem ViewModel neu laden möchten, verwenden Sie die SavedStateHandle API. Wenn die Daten mit der Benutzeroberfläche zusammenhängen und nicht im ViewModel gespeichert werden müssen, können Sie stattdessen onSaveInstanceState() verwenden. 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 möglicherweise kein ViewModel verwenden.

Gespeicherten Instanzstatus als Backup verwenden, um systemseitig initiierte Prozessbeendigung zu verarbeiten

Der onSaveInstanceState()-Callback im View-System und SavedStateHandle in ViewModels speichern Daten, die zum erneuten Laden des Status eines UI-Controllers (z. B. einer Aktivität oder eines Fragments) erforderlich sind, wenn das System diesen Controller zerstört und später neu erstellt. Informationen zum Implementieren des gespeicherten Instanzstatus mit onSaveInstanceState finden Sie im Leitfaden zum Aktivitätslebenszyklus im Abschnitt Aktivitätsstatus speichern und wiederherstellen.

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

Verwenden Sie den gespeicherten Instanzstatus 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 daher den gespeicherten Instanzstatus, um eine minimale Menge an erforderlichen Daten wie eine ID zu speichern, um die Daten neu zu erstellen, die zum Wiederherstellen der Benutzeroberfläche in ihren vorherigen Zustand erforderlich sind, falls die anderen Persistenzmechanismen fehlschlagen. Die meisten Apps sollten dies implementieren, um den vom System initiierten Prozessabschluss zu verarbeiten.

Je nach Anwendungsfall Ihrer App müssen Sie den gespeicherten Instanzstatus möglicherweise gar nicht verwenden. So kann ein Browser den Nutzer beispielsweise genau zu der Webseite zurückbringen, die er sich angesehen hat, bevor er den Browser geschlossen hat. Wenn sich Ihre Aktivität so verhält, können Sie auf die Verwendung des gespeicherten Instanzstatus verzichten und stattdessen alles lokal speichern.

Wenn Sie eine Aktivität über einen Intent öffnen, wird das Extra-Bundle sowohl bei Konfigurationsänderungen als auch beim Wiederherstellen der Aktivität durch das System an die Aktivität übergeben.

In beiden Fällen sollten Sie weiterhin ein ViewModel verwenden, um zu vermeiden, dass bei einer Konfigurationsänderung Zyklen durch das erneute Laden von Daten aus der Datenbank verschwendet werden.

Wenn die zu erhaltenden UI-Daten einfach und klein sind, können Sie möglicherweise nur APIs für den gespeicherten Instanzstatus verwenden, um Ihre Statusdaten zu erhalten.

Über SavedStateRegistry auf den gespeicherten Status zugreifen

Ab Fragment 1.1.0 oder der transitiven Abhängigkeit Activity 1.0.0 implementieren UI-Controller wie Activity oder Fragment SavedStateRegistryOwner und stellen ein SavedStateRegistry bereit, das an diesen Controller gebunden ist. Mit SavedStateRegistry können Komponenten in den gespeicherten Status des UI-Controllers eingebunden werden, um ihn zu nutzen oder zu erweitern. Das Saved State-Modul für ViewModel verwendet beispielsweise SavedStateRegistry, um ein SavedStateHandle zu erstellen und es Ihren ViewModel-Objekten zur Verfügung zu stellen. Sie können die SavedStateRegistry in Ihrem UI-Controller abrufen, indem Sie getSavedStateRegistry aufrufen.

Komponenten, die zum gespeicherten Status beitragen, müssen SavedStateRegistry.SavedStateProvider implementieren. Diese Schnittstelle definiert eine einzelne Methode namens saveState. Mit der Methode saveState() kann Ihre Komponente ein Bundle zurückgeben, das den Status enthält, der für diese Komponente gespeichert werden soll. SavedStateRegistry ruft diese Methode während der Phase „Status speichern“ des Lebenszyklus des UI-Controllers auf.

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

Um ein SavedStateProvider zu registrieren, rufen Sie registerSavedStateProvider() auf dem SavedStateRegistry auf und übergeben Sie einen Schlüssel, der den Daten des Anbieters zugeordnet werden soll, sowie den Anbieter. Die zuvor gespeicherten Daten für den Anbieter können aus dem gespeicherten Status abgerufen werden, indem consumeRestoredStateForKey() für SavedStateRegistry aufgerufen wird und der Schlüssel übergeben wird, der mit den Daten des Anbieters verknüpft ist.

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

Kotlin

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 SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

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

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

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

Lokale Persistenz verwenden, um das Beenden von Prozessen bei komplexen oder großen Daten zu verarbeiten

Nichtflüchtiger lokaler Speicher wie eine Datenbank oder gemeinsame Einstellungen bleiben 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. Dieser lokale Speicher überdauert zwar vom System initiierte Aktivitäten und das Beenden von Anwendungsprozessen, das Abrufen kann jedoch kostspielig sein, da die Daten aus dem lokalen Speicher in den Arbeitsspeicher gelesen werden müssen. Oft ist dieser persistente lokale Speicher bereits Teil Ihrer Anwendungsarchitektur, um alle Daten zu speichern, die Sie nicht verlieren möchten, wenn Sie die Aktivität öffnen und schließen.

Weder ViewModel noch der gespeicherte Instanzstatus sind langfristige Speicherlösungen und ersetzen daher nicht den lokalen Speicher, z. B. eine Datenbank. Stattdessen sollten Sie diese Mechanismen nur zum vorübergehenden Speichern von transientem UI-Status und nichtflüchtigen Speicher für andere App-Daten verwenden. Weitere Informationen dazu, wie Sie lokalen Speicher verwenden können, um die Daten Ihres App-Modells langfristig zu speichern (z.B. über Neustarts des Geräts hinweg), finden Sie im Leitfaden zur App-Architektur.

UI-Zustand 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 in jedem dieser Mechanismen ein anderer Datentyp gespeichert werden, der in der Aktivität verwendet wird. Das hängt von den Kompromissen zwischen Datenkomplexität, Zugriffsgeschwindigkeit und Lebensdauer ab:

  • Lokale Persistenz: Speichert alle Anwendungsdaten, die nicht verloren gehen sollen, wenn Sie die Aktivität öffnen und schließen.
    • Beispiel: Eine Sammlung von Song-Objekten, die Audiodateien und Metadaten enthalten kann.
  • ViewModel: Speichert im Arbeitsspeicher alle Daten, die zum Anzeigen der zugehörigen Benutzeroberfläche, des UI-Status des Bildschirms, erforderlich sind.
    • Beispiel: Die Song-Objekte der letzten Suche und die letzte Suchanfrage.
  • Gespeicherter Instanzstatus: Speichert eine kleine Menge an Daten, die zum erneuten Laden des UI-Status erforderlich sind, wenn das System die Benutzeroberfläche beendet und dann neu erstellt. Anstatt komplexe Objekte hier zu speichern, sollten Sie sie im lokalen Speicher beibehalten und eine eindeutige ID für diese Objekte in den APIs für den gespeicherten Instanzstatus speichern.
    • Beispiel: Speichern der letzten Suchanfrage.

Nehmen wir als Beispiel eine Aktivität, mit der Sie Ihre Musikbibliothek durchsuchen können. So sollten verschiedene Ereignisse behandelt werden:

Wenn der Nutzer einen Titel hinzufügt, delegiert ViewModel das lokale Speichern dieser Daten sofort. Wenn der neu hinzugefügte Song in der Benutzeroberfläche angezeigt werden soll, müssen Sie auch die Daten im ViewModel-Objekt aktualisieren, um die Hinzufügung des Songs zu berücksichtigen. Denken Sie daran, alle Datenbankeinfügungen außerhalb des Haupt-Threads vorzunehmen.

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

Wenn die Aktivität in den Hintergrund verschoben wird und das System die APIs für den gespeicherten Instanzstatus aufruft, sollte die Suchanfrage im gespeicherten Instanzstatus gespeichert werden, falls der Prozess neu erstellt wird. Da die Informationen zum Laden von Anwendungsdaten erforderlich sind, die darin gespeichert sind, speichern Sie die Suchanfrage im ViewModel SavedStateHandle. Das sind alle Informationen, die Sie benötigen, um die Daten zu laden und die Benutzeroberfläche in ihren aktuellen Zustand zurückzuversetzen.

Komplexe Zustände wiederherstellen: Teile wieder zusammensetzen

Wenn der Nutzer zur Aktivität zurückkehren soll, gibt es zwei mögliche Szenarien für die Wiederherstellung der Aktivität:

  • Die Aktivität wird neu erstellt, nachdem sie vom System beendet wurde. Das System hat die Abfrage in einem gespeicherten Instanzstatus-Bundle gespeichert und die Benutzeroberfläche sollte die Abfrage an ViewModel übergeben, wenn SavedStateHandle nicht verwendet wird. Der ViewModel stellt fest, dass keine Suchergebnisse im Cache gespeichert sind, und delegiert das Laden der Suchergebnisse mit der angegebenen Suchanfrage.
  • Die Aktivität wird nach einer Konfigurationsänderung neu erstellt. Da die ViewModel-Instanz nicht zerstört wurde, sind alle Informationen im Arbeitsspeicher der ViewModel zwischengespeichert und die Datenbank muss nicht noch einmal abgefragt werden.

Zusätzliche Ressourcen

Weitere Informationen zum Speichern von UI-Zuständen finden Sie in den folgenden Ressourcen.

Blogs

Codelabs