UI-Status in „Compose“ speichern

Je nachdem, wohin der Status hochgezogen wird und welche Logik erforderlich ist, können Sie verschiedene APIs verwenden, um den UI-Status zu speichern und wiederherzustellen. Jede App verwendet eine Kombination aus APIs, um dies bestmöglich zu erreichen.

Bei jeder Android-App kann der UI-Status aufgrund einer Aktivität oder Prozessneuerstellung verloren gehen. Dieser Verlust des Status kann aufgrund der folgenden Ereignisse auftreten:

Das Beibehalten des Zustands nach diesen Ereignissen ist für eine positive Nutzererfahrung unerlässlich. Welcher Status beibehalten werden soll, hängt von den individuellen User-Flows Ihrer App ab. Als Best Practice sollten Sie zumindest Nutzereingaben und den navigationsbezogenen Status beibehalten. Beispiele hierfür sind die Scrollposition einer Liste, die ID des Elements, zu dem der Nutzer weitere Details sehen möchte, die laufende Auswahl von Nutzereinstellungen oder die Eingabe in Textfeldern.

Auf dieser Seite werden die APIs zusammengefasst, die zum Speichern des UI-Status verfügbar sind, je nachdem, wohin der Status hochgezogen wird und welche Logik ihn benötigt.

UI-Logik

Wenn der Status in der UI hochgezogen wird, entweder in zusammensetzbaren Funktionen oder in einfachen State Holder-Klassen, die auf die Komposition beschränkt sind, können Sie rememberSaveable verwenden, um den Status bei der Neuerstellung von Aktivitäten und Prozessen beizubehalten.

Im folgenden Snippet wird rememberSaveable verwendet, um den Status eines einzelnen booleschen UI-Elements zu speichern:

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Abbildung 1: Die Chatnachrichten-Bubble wird maximiert und minimiert, wenn du darauf tippst.

showDetails ist eine boolesche Variable, die speichert, ob die Chat-Bubble minimiert oder maximiert ist.

rememberSaveable speichert den Status des UI-Elements in einem Bundle über den Mechanismus für den gespeicherten Instanzstatus.

Primitive Typen können automatisch im Bundle gespeichert werden. Wenn der Status in einem nicht primitiven Typ wie einer Datenklasse gespeichert ist, können Sie verschiedene Speichermechanismen verwenden, z. B. die Annotation Parcelize, Compose APIs wie listSaver und mapSaver oder eine benutzerdefinierte Saver-Klasse, die die Compose-Laufzeitklasse Saver erweitert. Weitere Informationen zu diesen Methoden finden Sie in der Dokumentation zu den Möglichkeiten zum Speichern des Status.

Im folgenden Snippet speichert die Compose API rememberLazyListState den Wert LazyListState, der aus dem Scrollstatus einer LazyColumn oder LazyRow besteht, mit rememberSaveable. Dabei wird ein LazyListState.Saver verwendet, ein benutzerdefinierter Saver, der den Scrollstatus speichern und wiederherstellen kann. Nach der Neuerstellung einer Aktivität oder eines Prozesses (z. B. nach einer Konfigurationsänderung wie dem Ändern der Geräteausrichtung) wird der Scrollstatus beibehalten.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Best Practice

rememberSaveable verwendet einen Bundle, um den UI-Status zu speichern. Dieser wird von anderen APIs gemeinsam genutzt, die ebenfalls darauf schreiben, z. B. onSaveInstanceState()-Aufrufe in Ihrer Aktivität. Die Größe dieses Bundle ist jedoch begrenzt und das Speichern großer Objekte kann zu TransactionTooLarge-Ausnahmen in der Laufzeit führen. Dies kann insbesondere bei Activity-Anwendungen mit nur einer Seite problematisch sein, bei denen derselbe Bundle in der gesamten Anwendung verwendet wird.

Um diese Art von Absturz zu vermeiden, sollten Sie keine großen komplexen Objekte oder Listen von Objekten im Bundle speichern.

Speichere stattdessen den erforderlichen Mindeststatus, z. B. IDs oder Schlüssel, und verwende diese, um die Wiederherstellung komplexerer UI-Zustände an andere Mechanismen wie nichtflüchtigen Speicher zu delegieren.

Diese Designentscheidungen hängen von den spezifischen Anwendungsfällen für Ihre App und davon ab, wie Ihre Nutzer das Verhalten der App erwarten.

Statuswiederherstellung prüfen

Sie können prüfen, ob der mit rememberSaveable in Ihren Compose-Elementen gespeicherte Status korrekt wiederhergestellt wird, wenn die Aktivität oder der Prozess neu erstellt wird. Dafür gibt es spezielle APIs, z. B. StateRestorationTester. Weitere Informationen finden Sie in der Dokumentation zum Testen.

Geschäftslogik

Wenn der Zustand des UI-Elements aufgrund der Geschäftslogik in das ViewModel-Element hochgezogen wird, können Sie die APIs von ViewModel verwenden.

Einer der Hauptvorteile der Verwendung eines ViewModel in Ihrer Android-App ist, dass Konfigurationsänderungen automatisch verarbeitet werden. Wenn sich die Konfiguration ändert und die Aktivität gelöscht und neu erstellt wird, bleibt der an ViewModel übergebene UI-Status im Arbeitsspeicher erhalten. Nach der Neuerstellung wird die alte ViewModel-Instanz an die neue Aktivitätsinstanz angehängt.

Eine ViewModel-Instanz überlebt jedoch keine vom System initiierte Prozessbeendigung. Damit der UI-Status dies übersteht, verwenden Sie das Saved State-Modul für ViewModel, das die SavedStateHandle API enthält.

Best Practice

SavedStateHandle verwendet ebenfalls den Bundle-Mechanismus, um den UI-Status zu speichern. Sie sollten ihn daher nur zum Speichern des einfachen UI-Elementstatus verwenden.

Der UI-Status des Bildschirms, der durch die Anwendung von Geschäftsregeln und den Zugriff auf andere Ebenen Ihrer Anwendung als die UI erzeugt wird, sollte aufgrund seiner potenziellen Komplexität und Größe nicht in SavedStateHandle gespeichert werden. Sie können verschiedene Mechanismen verwenden, um komplexe oder große Daten zu speichern, z. B. lokalen nichtflüchtigen Speicher. Nach der Neuerstellung des Prozesses wird der Bildschirm mit dem wiederhergestellten vorübergehenden Zustand neu erstellt, der in SavedStateHandle gespeichert wurde (falls vorhanden). Der UI-Zustand des Bildschirms wird dann wieder aus der Datenschicht erzeugt.

APIs von SavedStateHandle

SavedStateHandle bietet verschiedene APIs zum Speichern des Status von UI-Elementen, insbesondere:

State verfassen saveable()
StateFlow getStateFlow()

State schreiben

Verwenden Sie die saveable API von SavedStateHandle, um den Status von UI-Elementen als MutableState zu lesen und zu schreiben, damit er bei der Neuerstellung von Aktivitäten und Prozessen mit minimalem Codeaufwand erhalten bleibt.

Die saveable API unterstützt primitive Typen standardmäßig und empfängt einen stateSaver-Parameter, um benutzerdefinierte Saver zu verwenden, genau wie rememberSaveable().

Im folgenden Snippet speichert message die Nutzereingabe, die in ein TextField eingegeben wurde:

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

Weitere Informationen zur Verwendung der saveable API finden Sie in der Dokumentation zu SavedStateHandle.

StateFlow

Verwenden Sie getStateFlow(), um den Status des UI-Elements zu speichern und als Ablauf aus dem SavedStateHandle zu nutzen. Der StateFlow ist schreibgeschützt und die API erfordert, dass Sie einen Schlüssel angeben, damit Sie den Ablauf ersetzen können, um einen neuen Wert auszugeben. Mit dem von Ihnen konfigurierten Schlüssel können Sie den StateFlow abrufen und den neuesten Wert erfassen.

Im folgenden Snippet ist savedFilterType eine StateFlow-Variable, in der ein Filtertyp gespeichert wird, der auf eine Liste von Chatkanälen in einer Chat-App angewendet wird:

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

Jedes Mal, wenn der Nutzer einen neuen Filtertyp auswählt, wird setFiltering aufgerufen. Dadurch wird ein neuer Wert in SavedStateHandle gespeichert, der mit dem Schlüssel _CHANNEL_FILTER_SAVED_STATE_KEY_ gespeichert ist. savedFilterType ist ein Flow, der den neuesten Wert ausgibt, der für den Schlüssel gespeichert wurde. filteredChannels hat den Ablauf abonniert, um die Kanalfilterung durchzuführen.

Weitere Informationen zur getStateFlow() API finden Sie in der Dokumentation zu SavedStateHandle.

Zusammenfassung

In der folgenden Tabelle sind die in diesem Abschnitt behandelten APIs zusammengefasst und es wird beschrieben, wann sie jeweils zum Speichern des UI-Status verwendet werden sollten:

Ereignis UI-Logik Geschäftslogik in einem ViewModel
Konfigurationsänderungen rememberSaveable Automatisch
Vom System initiierte Prozessbeendigung rememberSaveable SavedStateHandle

Welche API verwendet werden soll, hängt davon ab, wo der Status gespeichert ist und welche Logik erforderlich ist. Verwenden Sie für den Status, der in der UI-Logik verwendet wird, rememberSaveable. Wenn Sie den Status, der in der Geschäftslogik verwendet wird, in einem ViewModel speichern, speichern Sie ihn mit SavedStateHandle.

Verwenden Sie die Bundle APIs (rememberSaveable und SavedStateHandle), um kleine Mengen an UI-Zuständen zu speichern. Diese Daten sind zusammen mit anderen Speichermechanismen das Minimum, das erforderlich ist, um die Benutzeroberfläche wieder in ihren vorherigen Zustand zu versetzen. Wenn Sie beispielsweise die ID eines Profils, das sich der Nutzer angesehen hat, im Bundle speichern, können Sie umfangreiche Daten wie Profildetails aus der Datenschicht abrufen.

Weitere Informationen zu den verschiedenen Möglichkeiten, den UI-Status zu speichern, finden Sie in der allgemeinen Dokumentation zum Speichern des UI-Status und auf der Seite Datenebene des Architekturleitfadens.