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 erläutert.

Das Speichern und Wiederherstellen des UI-Status einer Aktivität kurz 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 beenden.

Um die Lücke zwischen den Erwartungen der Nutzer und dem Systemverhalten zu schließen, 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 der Drehung des Geräts.

Erwartungen der Nutzer und Systemverhalten

Je nach Aktion des Nutzers erwarten sie, dass der Aktivitätsstatus 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 einer Aktivität gleich bleibt, bis der Nutzer die Aktivität vollständig beendet. Der Nutzer kann eine Aktivität vollständig beenden, indem er Folgendes tut:

  • Die Aktivität vom Bildschirm „Übersicht“ (Letzte) 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 die Aktivität dauerhaft verlassen hat. Wenn er die Aktivität wieder öffnet, erwartet er, dass sie in einem sauberen Status startet. Das zugrunde liegende Systemverhalten für diese Löschszenarien entspricht den Erwartungen des Nutzers: Die Aktivitätsinstanz wird beendet 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 Beenden. Beispielsweise erwartet ein Nutzer möglicherweise, dass ein Browser ihn genau zu der Webseite führt, 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 einer Aktivität bei einer Konfigurationsänderung wie der Drehung des Geräts oder dem Wechsel in den Multifenstermodus gleich bleibt. Standardmäßig beendet das System jedoch die Aktivität, wenn eine solche Konfigurationsänderung auftritt, und löscht alle UI-Status, die in der Aktivitätsinstanz gespeichert sind. Weitere Informationen zu Gerätekonfigurationen finden Sie auf der Referenzseite zur Konfiguration.

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 Aktivität 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 in Ihrer Suchaktivität aus und drückt dann die Home-Taste oder nimmt einen Anruf entgegen. Wenn er zur Suchaktivität zurückkehrt, erwartet er, dass der Suchbegriff und die Ergebnisse noch genau so vorhanden sind wie zuvor.

In diesem Szenario wird Ihre App in den Hintergrund versetzt und das System versucht, den App-Prozess im Arbeitsspeicher zu behalten. Das System kann den Anwendungsprozess jedoch beenden, während der Nutzer mit anderen Apps interagiert. In diesem Fall wird die Aktivitätsinstanz zusammen mit allen darin gespeicherten Status beendet. Wenn der Nutzer die App neu startet, befindet sich die Aktivität unerwartet in einem sauberen Status. Weitere Informationen zum Beenden von Prozessen 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 Instanzstatus

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 Beenden der Aktivität durch den Nutzer/finish()

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 es zu einer Drehung des Geräts, einer Größenänderung des Fensters oder anderen häufig auftretenden Konfigurationsänderungen kommt. Informationen zum Implementieren eines ViewModel finden Sie im ViewModel-Leitfaden.

ViewModel behält die Daten im Arbeitsspeicher. Daher ist der Abruf günstiger als der Abruf von Daten von der Festplatte oder aus dem Netzwerk. Ein ViewModel ist mit einer Activity (oder einem anderen Lebenszyklusinhaber) verknüpft. Es bleibt bei einer Konfigurationsänderung im Arbeitsspeicher und das System verknüpft das ViewModel automatisch mit der neuen Activity-Instanz, die sich aus der Konfigurationsänderung ergibt.

ViewModels werden vom System automatisch beendet, wenn der Nutzer die Aktivität oder das 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 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 alternativ onSaveInstanceState(). 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 Instanzstatus als Backup verwenden, um vom System initiierte Prozessbeendigungen zu verarbeiten

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

Gespeicherte Instanzstatus-Bundles bleiben sowohl bei Konfigurationsänderungen als auch bei Prozessbeendigungen erhalten, sind aber 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 bei einer Konfigurationsänderung im Hauptthread erfolgt, kann eine lange 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 den gespeicherten Instanzstatus, 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 Status 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 Instanzstatus 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 Instanzstatus verzichten und stattdessen alles lokal speichern.

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.

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

In Fällen, in denen 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.

Mit SavedStateRegistry in den gespeicherten Status einbinden

Ab Fragment 1.1.0 oder der transitiven Abhängigkeit Activity 1.0.0 implementieren UI-Controller wie ein Activity oder Fragment SavedStateRegistryOwner und stellen eine SavedStateRegistry bereit, die an diesen Controller gebunden ist. SavedStateRegistry ermöglicht es Komponenten, sich in den gespeicherten Status des UI-Controllers einzubinden, 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 UI-Controller abrufen, indem Sie getSavedStateRegistry 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 von dieser Komponente gespeichert werden sollen. SavedStateRegistry ruft diese Methode während der Phase des Speicherns des Status im 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;
    }
}

Rufen Sie registerSavedStateProvider() in der SavedStateRegistry auf, um ein SavedStateProvider zu registrieren. Übergeben Sie 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 Activity oder einem Fragment 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 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 Prozessbeendigungen für komplexe oder große Daten zu verarbeiten

Nichtflüchtiger lokaler Speicher wie eine Datenbank oder freigegebene Einstellungen 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 von Aktivitäten und 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, 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 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 zum langfristigen Speichern Ihrer App-Modelldaten (z.B. bei Neustarts des Geräts) finden Sie im Leitfaden zur App-Architektur .

UI-Status verwalten: Teilen und herrschen

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 Aktivität 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 Aktivität ö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, 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 hier komplexe Objekte zu speichern, speichern Sie die komplexen Objekte im lokalen Speicher und speichern Sie eine eindeutige ID für diese Objekte in den APIs für den gespeicherten Instanzstatus.
    • Beispiel: Die letzte Suchanfrage speichern.

Nehmen wir als Beispiel eine Aktivität, 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 das Speichern dieser Daten lokal. Wenn dieser neu hinzugefügte Song in der Benutzeroberfläche angezeigt werden soll, sollten 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 Aktivität in den Hintergrund versetzt 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 erforderlich sind, um Anwendungsdaten zu laden, die hier gespeichert sind, speichern Sie die Suchanfrage im ViewModel SavedStateHandle. Das sind alle Informationen, die Sie zum Laden der Daten und zum Zurückversetzen der Benutzeroberfläche in den aktuellen Status benötigen.

Komplexe Status wiederherstellen: Teile wieder zusammensetzen

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

  • Die Aktivität wird neu erstellt, nachdem sie vom System beendet wurde. Die Abfrage ist vom System in einem gespeicherten Instanzstatus-Bundle gespeichert und die Benutzeroberfläche sollte die Abfrage an das ViewModel übergeben, wenn SavedStateHandle nicht verwendet wird. Das ViewModel erkennt, 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 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.

Blogs

Codelabs