UI-Zustände speichern

In diesem Leitfaden werden die Erwartungen der Nutzer in Bezug auf den UI-Status und die Optionen erläutert, mit denen der Status beibehalten werden kann.

Für eine gute Nutzererfahrung ist es wichtig, den UI-Status einer Aktivität schnell zu speichern und wiederherzustellen, nachdem das System Aktivitäten oder Anwendungen gelöscht hat. Nutzer erwarten, dass der UI-Status gleich bleibt. Das System kann jedoch die Aktivität und den gespeicherten Status löschen.

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

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

Ihre App muss die Erwartungen der Nutzer erfüllen und eine schnelle, reaktionsschnelle Oberfläche bieten. Vermeiden Sie Verzögerungen beim Laden von Daten in die UI, insbesondere nach gängigen Konfigurationsänderungen wie der Rotation.

Erwartungen der Nutzenden und Systemverhalten

Abhängig von der Aktion, die ein Nutzer ausführt, erwartet er entweder, dass der Aktivitätsstatus gelöscht oder der Status beibehalten wird. Manchmal tut das System automatisch das, was der Nutzer erwartet. In anderen Fällen tut das System das Gegenteil von dem, was die Nutzenden erwarten.

Vom Nutzer initiierte Ablehnung des UI-Status

Der Nutzer erwartet, dass beim Start einer Aktivität der vorübergehende UI-Status dieser Aktivität unverändert bleibt, bis der Nutzer die Aktivität vollständig abschließt. So kann er eine Aktivität komplett verwerfen:

  • Die Aktivität vom Bildschirm „Übersicht (Letzte)“ wegwischen
  • Beenden der App über den Bildschirm „Einstellungen“ oder erzwungenes Beenden
  • Das Gerät wird neu gestartet.
  • Der Abschluss einer Art „abgeschlossener“ Aktion (wird durch Activity.finish() unterstützt).

Der Nutzer geht bei diesen vollständigen Ablehnungsfällen davon aus, dass er die Aktivität dauerhaft verlassen hat. Wenn er die Aktivität wieder öffnet, erwartet er, dass die Aktivität in einem bereinigten Zustand gestartet wird. Das zugrunde liegende Systemverhalten für diese Ablehnungsszenarien entspricht den Erwartungen des Nutzers: Die Aktivitätsinstanz wird zusammen mit dem darin gespeicherten Status und allen gespeicherten Instanzstatusdaten, die mit der Aktivität verknüpft sind, gelöscht und aus dem Arbeitsspeicher entfernt.

Es gibt einige Ausnahmen von dieser Regel in Bezug auf eine vollständige Ablehnung. Beispielsweise könnte ein Nutzer erwarten, dass ein Browser ihn auf genau die Webseite weiterleitet, die er sich angesehen hat, bevor er den Browser über die Schaltfläche „Zurück“ verlässt.

Vom System initiierte Ablehnung des UI-Status

Ein Nutzer erwartet, dass der UI-Status einer Aktivität während einer Konfigurationsänderung, z. B. bei Drehung oder Wechsel in den Mehrfenstermodus, gleich bleibt. Standardmäßig löscht das System die Aktivität jedoch bei einer solchen Konfigurationsänderung. Dabei werden alle in der Aktivitätsinstanz gespeicherten UI-Zustände gelöscht. Weitere Informationen zu Gerätekonfigurationen finden Sie auf der Referenzseite zur Konfiguration. Es ist zwar möglich, aber nicht empfehlenswert, das Standardverhalten bei Konfigurationsänderungen zu überschreiben. Weitere Informationen finden Sie unter Konfigurationsänderung selbst vornehmen.

Ein Nutzer erwartet außerdem, dass der UI-Status deiner Aktivität gleich bleibt, wenn er vorübergehend zu einer anderen App wechselt und später zu deiner App zurückkehrt. Beispiel: Der Nutzer führt eine Suche in Ihrer Suchaktivität durch und drückt dann die Startbildschirmtaste oder nimmt einen Anruf an. Wenn er zu der Suchaktivität zurückkehrt, erwartet er, dass der Suchbegriff und die Ergebnisse weiterhin vorhanden sind, genau wie zuvor.

In diesem Szenario wird die Anwendung im Hintergrund platziert. Das System versucht, den Anwendungsprozess im Arbeitsspeicher zu halten. Das System kann jedoch den Anwendungsprozess zerstören, während der Nutzer nicht mit anderen Anwendungen interagiert. In einem solchen Fall wird die Aktivitätsinstanz zusammen mit jedem darin gespeicherten Status gelöscht. Wenn der Nutzer die App neu startet, ist die Aktivität unerwartet in einem bereinigten Zustand. Weitere Informationen zum Löschen von Prozessen finden Sie unter Prozesse und Anwendungslebenszyklus.

Optionen zum Beibehalten des UI-Status

Wenn die Erwartungen des Nutzers in Bezug auf den UI-Status nicht mit dem Standardsystemverhalten übereinstimmen, müssen Sie den UI-Status des Nutzers speichern und wiederherstellen. So sorgen Sie dafür, dass das vom System initiierte Löschen für den Nutzer transparent ist.

Die Optionen zum Beibehalten des UI-Status variieren in Abhängigkeit von den folgenden Dimensionen, die sich auf die Nutzererfahrung auswirken:

ViewModel Status der gespeicherten Instanz Nichtflüchtiger Speicher
Speicherort im Arbeitsspeicher im Arbeitsspeicher auf Laufwerk oder Netzwerk
Konfigurationsänderung bleibt erhalten Ja Ja Ja
Überlebt den vom System initiierten Prozesstod Nein Ja Ja
Überlebt, ob Nutzer die Aktivität verworfen/onFinish() abgeschlossen hat Nein Nein Ja
Dateneinschränkungen Komplexe Objekte sind zulässig, aber der Platz ist durch den verfügbaren Arbeitsspeicher begrenzt nur für primitive Typen und einfache, kleine Objekte wie String nur durch Speicherplatz oder Kosten bzw. Abrufzeit von der Netzwerkressource begrenzt
Lese-/Schreibzeit schnell (nur Arbeitsspeicherzugriff) langsam (erfordert Serialisierung/Deserialisierung) langsam (erfordert Laufwerkzugriff oder Netzwerktransaktion)

ViewModel zur Verarbeitung von Konfigurationsänderungen verwenden

ViewModel ist ideal zum Speichern und Verwalten von UI-bezogenen Daten, während der Nutzer die Anwendung aktiv nutzt. Sie ermöglicht den schnellen Zugriff auf UI-Daten und hilft Ihnen, das erneute Abrufen von Daten vom Netzwerk oder Laufwerk durch Rotation, Fenstergröße und andere häufig auftretende Konfigurationsänderungen zu vermeiden. Informationen zum Implementieren eines ViewModel-Objekts finden Sie in der Anleitung zu ViewModel.

ViewModel behält die Daten im Arbeitsspeicher, was bedeutet, dass sie kostengünstiger als Daten vom Laufwerk oder Netzwerk abgerufen werden können. Ein ViewModel ist einer Aktivität (oder einem anderen Lebenszyklusinhaber) zugeordnet. Sie bleibt während einer Konfigurationsänderung im Arbeitsspeicher und das System ordnet die ViewModel automatisch der neuen Aktivitätsinstanz zu, die aus der Konfigurationsänderung resultiert.

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

Im Gegensatz zum Status einer gespeicherten Instanz werden ViewModels während eines vom System initiierten Prozesses gelöscht. Mit der SavedStateHandle API können Sie Daten nach einem vom System initiierten Prozessende in einem ViewModel neu laden. Wenn sich die Daten auf die Benutzeroberfläche beziehen und nicht in der ViewModel-Datei enthalten sein müssen, verwenden Sie alternativ onSaveInstanceState() im View-System oder rememberSaveable in Jetpack Compose. Wenn es sich um Anwendungsdaten handelt, ist es möglicherweise besser, sie auf dem Laufwerk zu speichern.

Wenn Sie bereits eine speicherinterne Lösung haben, um den UI-Status über Konfigurationsänderungen hinweg zu speichern, müssen Sie ViewModel möglicherweise nicht verwenden.

Status „Gespeicherte Instanz“ als Sicherung verwenden, um den vom System initiierten Prozess zu beenden

Der onSaveInstanceState()-Callback im View-System, rememberSaveable in Jetpack Compose und SavedStateHandle in ViewModels speichern Daten, die zum Aktualisieren des Status eines UI-Controllers erforderlich sind, z. B. eine Aktivität oder ein Fragment, wenn das System diesen Controller löscht und später neu erstellt. Informationen zum Implementieren des gespeicherten Instanzstatus mit onSaveInstanceState finden Sie im Leitfaden zum Aktivitätslebenszyklus unter Aktivitätsstatus speichern und wiederherstellen.

Gespeicherte Instanzstatus-Bundles bleiben sowohl durch Konfigurationsänderungen als auch durch Prozessfehler bestehen, sind jedoch durch den Speicher und die Geschwindigkeit eingeschränkt, da die verschiedenen APIs Daten virtualisieren. Die Serialisierung kann viel Arbeitsspeicher verbrauchen, wenn die Objekte, die zu einem anderen Standard ausgeführt werden, kompliziert sind. Da dieser Prozess während einer Konfigurationsänderung im Hauptthread stattfindet, kann eine lang andauernde Serialisierung dazu führen, dass Frames ausgelassen werden und es zu Unterbrechungen kommt.

Verwenden Sie den gespeicherten Instanzstatus nicht, um große Datenmengen wie Bitmaps oder komplexe Datenstrukturen zu speichern, die eine langwierige 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 Datenmenge zu speichern, z. B. eine ID, um die Daten neu zu erstellen, die zum Wiederherstellen des vorherigen Zustands der UI erforderlich sind, falls die anderen Persistenzmechanismen fehlschlagen. In den meisten Apps sollte dies implementiert werden, um den vom System initiierten Prozesstod abzufangen.

Je nach den Anwendungsfällen Ihrer Anwendung müssen Sie den Status der gespeicherten Instanz möglicherweise gar nicht verwenden. Beispielsweise kann ein Browser den Nutzer vor dem Verlassen des Browsers zu genau der Webseite zurückführen, die er sich angesehen hat. Wenn sich Ihre Aktivität so verhält, können Sie auf den Status der gespeicherten Instanz verzichten und stattdessen alles lokal beibehalten.

Wenn Sie eine Aktivität aus einem Intent öffnen, wird das Paket mit Extras außerdem an die Aktivität übergeben, wenn sich die Konfiguration ändert und wenn das System die Aktivität wiederherstellt. Wenn ein Element der UI-Statusdaten, z. B. eine Suchanfrage, beim Start der Aktivität als Extra-Intent übergeben wurde, können Sie das Extra-Bundle anstelle des gespeicherten Instanzstatus-Bundles verwenden. Weitere Informationen zu Intent-Extras finden Sie unter Intent- und Intent-Filter.

In beiden Fällen sollten Sie dennoch einen ViewModel verwenden, um zu vermeiden, dass bei einer Konfigurationsänderung Daten aus der Datenbank neu geladen werden müssen.

In Fällen, in denen die beizubehaltenden UI-Daten einfach und schlank sind, können Sie auch nur gespeicherte Instanzstatus-APIs verwenden, um die Statusdaten beizubehalten.

Mit SavedStateRegistry in gespeichertem Zustand eingreifen

Ab Fragment 1.1.0 oder seiner vorübergehenden Abhängigkeit Activity 1.0.0 implementieren UI-Controller wie Activity oder Fragment SavedStateRegistryOwner und stellen einen SavedStateRegistry bereit, der an diesen Controller gebunden ist. Mit SavedStateRegistry können Komponenten in den gespeicherten Status Ihres UI-Controllers eingebunden werden, um ihn zu nutzen oder zu ihm beizutragen. Das Modul „Gespeicherter Status“ für ViewModel verwendet beispielsweise SavedStateRegistry, um eine SavedStateHandle zu erstellen und für Ihre ViewModel-Objekte bereitzustellen. Sie können die SavedStateRegistry in Ihrem UI-Controller durch Aufrufen von getSavedStateRegistry() abrufen.

Komponenten, die zum gespeicherten Status beitragen, müssen SavedStateRegistry.SavedStateProvider implementieren, wodurch eine einzelne Methode namens saveState() definiert wird. Mit der Methode saveState() kann Ihre Komponente ein Bundle-Objekt zurückgeben, das jeden Status enthält, der von dieser Komponente gespeichert werden soll. SavedStateRegistry ruft diese Methode während der Speicherstatusphase des Lebenszyklus des UI-Controllers auf.

Kotlin

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

Java

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

Rufen Sie zum Registrieren eines SavedStateProviders registerSavedStateProvider() auf der SavedStateRegistry auf und übergeben Sie einen Schlüssel, der mit den Daten des Anbieters und dem Anbieter verknüpft werden soll. Die zuvor für den Anbieter gespeicherten Daten können aus dem gespeicherten Status abgerufen werden, indem consumeRestoredStateForKey() im SavedStateRegistry aufgerufen und der mit den Daten des Anbieters verknüpfte Schlüssel übergeben wird.

In Activity oder Fragment können Sie nach dem Aufrufen von super.onCreate() ein SavedStateProvider in onCreate() registrieren. Alternativ kannst du ein LifecycleObserver auf einem SavedStateRegistryOwner festlegen, das LifecycleOwner implementiert, und das SavedStateProvider registrieren, sobald das ON_CREATE-Ereignis eintritt. Mit einem LifecycleObserver können Sie die Registrierung und das Abrufen 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 nutzen, um mit dem Tod von komplexen oder großen Daten umzugehen

Ein nichtflüchtiger lokaler Speicher, z. B. eine Datenbank oder freigegebene Einstellungen, bleibt so lange bestehen, wie Ihre App 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 überlebt zwar die vom System initiierte Aktivität und den Tod von Anwendungsprozessen, 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 zum Speichern aller Daten, die beim Öffnen und Schließen der Aktivität nicht verloren gehen sollen.

Weder ViewModel noch der Status der gespeicherten Instanz sind Langzeitspeicherlösungen und kein Ersatz für lokalen Speicher, wie z. B. eine Datenbank. Stattdessen sollten Sie diese Mechanismen nur zum vorübergehenden Speichern des vorübergehenden UI-Zustands verwenden und den nichtflüchtigen Speicher für andere Anwendungsdaten verwenden. Im Leitfaden zur App-Architektur finden Sie weitere Informationen zur Nutzung des lokalen Speichers, um die Daten Ihres App-Modells langfristig (z.B. nach Neustarts des Geräts) zu sichern.

UI-Status verwalten: Teilen und erobern

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 eine andere Art von Daten speichern, die in der Aktivität verwendet werden, basierend auf den Kompromissen in Bezug auf Datenkomplexität, Zugriffsgeschwindigkeit und Lebensdauer:

  • Lokale Persistenz: Speichert alle Anwendungsdaten, die nicht verloren gehen sollen, wenn Sie die Aktivität öffnen und schließen.
    • Beispiel: Eine Sammlung von Songobjekten, die Audiodateien und Metadaten enthalten können.
  • ViewModel: Speichert alle Daten, die zur Anzeige der zugehörigen UI, des Bildschirm-UI-Status, erforderlich sind.
    • Beispiel: Die Songobjekte der letzten und der letzten Suchanfrage.
  • Gespeicherter Instanzstatus: Speichert eine kleine Datenmenge, die zum Aktualisieren des UI-Status erforderlich ist, wenn das System beendet wird, und erstellt dann die UI neu. Anstatt hier komplexe Objekte zu speichern, können Sie die komplexen Objekte im lokalen Speicher beibehalten und eine eindeutige ID für diese Objekte in den gespeicherten Instanzstatus-APIs speichern.
    • Beispiel: Letzte Suchanfrage speichern.

Betrachten Sie als Beispiel eine Aktivität, mit der Sie Ihre Mediathek mit Titeln durchsuchen können. So sollten Sie mit verschiedenen Ereignissen umgehen:

Wenn der Nutzer einen Song hinzufügt, delegiert das ViewModel sofort die Speicherung dieser Daten lokal. Wenn dieser neu hinzugefügte Titel in der UI angezeigt werden soll, sollten Sie auch die Daten im ViewModel-Objekt aktualisieren, um den hinzugefügten Titel zu berücksichtigen. Denken Sie daran, alle Datenbank-Einfügungen außerhalb des Hauptthreads durchzuführen.

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

Wenn die Aktivität in den Hintergrund geht und das System die gespeicherten Instanzstatus-APIs aufruft, sollte die Suchanfrage im Status der gespeicherten Instanz gespeichert werden, für den Fall, dass der Prozess neu erstellt wird. Da die Informationen zum Laden der darin beibehaltenen Anwendungsdaten erforderlich sind, speichern Sie die Suchanfrage im ViewModel-SavedStateHandle. Dies sind alle Informationen, die Sie benötigen, um die Daten zu laden und die UI in ihren aktuellen Zustand zurückzusetzen.

Komplexe Status wiederherstellen: Teile wieder zusammensetzen

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

  • Die Aktivität wird neu erstellt, nachdem sie vom System gestoppt wurde. Die Abfrage ist im System in einem gespeicherten Instanzstatus-Bundle gespeichert. Wenn SavedStateHandle nicht verwendet wird, sollte die Abfrage von der UI an ViewModel übergeben werden. Der ViewModel erkennt, dass keine Suchergebnisse im Cache gespeichert sind, und delegiert das Laden der Suchergebnisse mithilfe der angegebenen Suchanfrage.
  • Die Aktivität wird nach einer Konfigurationsänderung erstellt. Da die Instanz ViewModel nicht gelöscht wurde, befinden sich alle Informationen im Cache von ViewModel und die Datenbank muss nicht noch einmal abgefragt werden.

Weitere Informationen

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

Blogs