UI-Ebene

Die Benutzeroberfläche dient dazu, die Anwendungsdaten auf dem Bildschirm anzuzeigen. Die Benutzeroberfläche ist auch der primäre Punkt für Nutzerinteraktionen. Immer wenn sich die Daten ändern, entweder durch Nutzerinteraktion (z. B. durch Drücken einer Schaltfläche) oder durch externe Eingaben (z. B. eine Netzwerkantwort), wird die Benutzeroberfläche entsprechend aktualisiert. Die Benutzeroberfläche ist im Grunde eine visuelle Darstellung des Anwendungsstatus, der aus der Datenschicht abgerufen wird.

Die Anwendungsdaten, die Sie aus der Datenschicht erhalten, haben jedoch in der Regel ein anderes Format als die Informationen, die Sie anzeigen müssen. Möglicherweise benötigen Sie für die Benutzeroberfläche nur einen Teil der Daten oder müssen zwei verschiedene Datenquellen zusammenführen, um für den Nutzer relevante Informationen zu präsentieren. Unabhängig von der angewendeten Logik müssen Sie der Benutzeroberfläche alle Informationen übergeben, die für das vollständige Rendern erforderlich sind. Die UI-Ebene ist die Pipeline, die Änderungen an Anwendungsdaten in ein Format umwandelt, das in der Benutzeroberfläche dargestellt werden kann, und die Daten dann anzeigt.

In einer typischen Architektur hängen die UI-Elemente der UI-Schicht von Status-Holdern ab, die wiederum von Klassen aus der Datenschicht oder der optionalen Domänenschicht abhängen.
Abbildung 1. Die Rolle der UI-Ebene in der App-Architektur.

Eine einfache Fallstudie

Stellen Sie sich eine App vor, die Nachrichtenartikel abruft, damit ein Nutzer sie lesen kann. Die App hat einen Artikelbildschirm, auf dem verfügbare Artikel angezeigt werden. Angemeldete Nutzer können außerdem besonders interessante Artikel mit einem Lesezeichen versehen. Da es jederzeit viele Artikel geben kann, muss der Leser Artikel nach Kategorie durchsuchen können. Zusammenfassend lässt sich sagen, dass Nutzer mit der App Folgendes tun können:

  • Artikel ansehen, die gelesen werden können
  • Artikel nach Kategorie durchsuchen
  • Melden Sie sich an und fügen Sie bestimmte Artikel zu Ihren Lesezeichen hinzu.
  • Bei entsprechender Berechtigung können Sie auf einige Premium-Funktionen zugreifen.
Eine Beispiel-Nachrichten-App mit Artikelvorschauen, von denen eine mit einem Lesezeichen versehen ist.
Abbildung 2. Eine Beispiel-Nachrichten-App für eine UI-Fallstudie.

In den folgenden Abschnitten wird dieses Beispiel als Fallstudie verwendet, um die Prinzipien des unidirektionalen Datenflusses vorzustellen und die Probleme zu veranschaulichen, die diese Prinzipien im Kontext der App-Architektur für die UI-Ebene lösen.

Architektur der UI-Ebene

Der Begriff UI bezieht sich auf UI-Elemente wie Container und komponierbare Funktionen, die Daten anzeigen. Für die Entwicklung von Android-Benutzeroberflächen empfehlen wir Jetpack Compose. Da die Datenebene dazu dient, auf die App-Daten zuzugreifen, sie zu verwalten und bereitzustellen, muss die UI-Ebene die folgenden Schritte ausführen:

  1. App-Daten nutzen und in Daten umwandeln, die von der Benutzeroberfläche problemlos gerendert werden können.
  2. UI-renderbare Daten aufnehmen und in UI-Elemente für die Darstellung für den Nutzer umwandeln.
  3. Verarbeiten Sie Nutzer-Eingabeereignisse aus diesen zusammengesetzten UI-Elementen und spiegeln Sie ihre Auswirkungen bei Bedarf in den UI-Daten wider.
  4. Wiederholen Sie die Schritte 1 bis 3 so oft wie nötig.

Im restlichen Teil dieses Leitfadens wird beschrieben, wie Sie eine UI-Ebene implementieren, die diese Schritte ausführt. In dieser Anleitung werden insbesondere die folgenden Aufgaben und Konzepte behandelt:

  • UI-Status definieren
  • Unidirektionaler Datenfluss (UDF) als Mittel zum Erstellen und Verwalten des UI-Zustands
  • UI-Status mit beobachtbaren Datentypen gemäß UDF-Prinzipien verfügbar machen
  • Benutzeroberfläche implementieren, die den beobachtbaren UI-Status nutzt

Der grundlegendste davon ist die Definition des UI-Zustands.

UI-Status definieren

In der Fallstudie oben wird in der Benutzeroberfläche eine Liste von Artikeln zusammen mit einigen Metadaten für jeden Artikel angezeigt. Diese Informationen, die die App dem Nutzer präsentiert, sind der UI-Status.

Wenn die Benutzeroberfläche das ist, was der Nutzer sieht, ist der UI-Status das, was die App ihm anzeigen sollte. Die Benutzeroberfläche ist wie zwei Seiten derselben Medaille die visuelle Darstellung des UI-Zustands. Alle Änderungen am UI-Status werden sofort in der Benutzeroberfläche angezeigt.

Die Benutzeroberfläche ist das Ergebnis der Bindung von UI-Elementen auf dem Bildschirm an den UI-Zustand.
Abbildung 3. Die Benutzeroberfläche ist das Ergebnis der Bindung von UI-Elementen auf dem Bildschirm an den UI-Status.

Betrachten Sie die Fallstudie: Um die Anforderungen der News-App zu erfüllen, können die Informationen, die zum vollständigen Rendern der Benutzeroberfläche erforderlich sind, in einer NewsUiState-Datenklasse gekapselt werden, die so definiert ist:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    // ...
)

Weitere Informationen zum UI-Status finden Sie unter State und Jetpack Compose.

Unveränderlichkeit

Die Definition des UI-Zustands im vorherigen Beispiel ist unveränderlich. Der Hauptvorteil besteht darin, dass unveränderliche Objekte Garantien für den Zustand der Anwendung zu einem bestimmten Zeitpunkt bieten. So kann sich die Benutzeroberfläche auf ihre primäre Rolle konzentrieren: den Status zu lesen und die UI-Elemente entsprechend zu aktualisieren. Ändern Sie den UI-Status niemals direkt in der Benutzeroberfläche, es sei denn, die Benutzeroberfläche selbst ist die einzige Quelle ihrer Daten. Ein Verstoß gegen dieses Prinzip führt zu mehreren Wahrheitsquellen für dieselben Informationen, was zu Dateninkonsistenzen und subtilen Fehlern führt.

Sehen Sie sich beispielsweise die Fallstudie von oben an. Wenn das bookmarked-Flag in einem NewsItemUiState-Objekt aus dem UI-Zustand in der Activity-Klasse aktualisiert wird, konkurriert dieses Flag mit der Datenschicht als Quelle für den Status „Mit Lesezeichen versehen“ eines Artikels. Unveränderliche Datenklassen sind sehr nützlich, um diese Art von Inkonsistenz zu vermeiden.

Namenskonventionen in diesem Leitfaden

In dieser Anleitung werden UI-Statusklassen nach der Funktionalität des Bildschirms oder des Teils des Bildschirms benannt, den sie beschreiben. Die Konvention lautet so:

Funktionalität + UiState.

Der Status eines Bildschirms, auf dem Nachrichten angezeigt werden, könnte beispielsweise NewsUiState heißen, und der Status eines Nachrichteneintrags in einer Liste von Nachrichteneinträgen könnte NewsItemUiState sein.

Status mit unidirektionalem Datenfluss verwalten

Im vorherigen Abschnitt wurde festgestellt, dass der UI-Zustand ein unveränderlicher Snapshot der Details ist, die für das Rendern der Benutzeroberfläche erforderlich sind. Aufgrund der dynamischen Natur von Daten in Apps kann sich der Status jedoch im Laufe der Zeit ändern. Das kann an Nutzerinteraktionen oder anderen Ereignissen liegen, die die zugrunde liegenden Daten ändern, mit denen die App gefüllt wird.

Für diese Interaktionen kann ein Mediator verwendet werden, um sie zu verarbeiten. Dabei wird die Logik definiert, die auf jedes Ereignis angewendet werden soll, und die zugrunde liegenden Datenquellen werden transformiert, um den UI-Status zu erstellen. Obwohl diese Interaktionen und ihre Logik in der Benutzeroberfläche selbst untergebracht werden können, kann dies schnell unübersichtlich werden, da die Benutzeroberfläche zu viele Aufgaben übernimmt. Außerdem kann sich dies auf die Testbarkeit auswirken, da der resultierende Code eng gekoppelt ist. Sofern der UI-Status nicht sehr einfach ist, sollte die UI nur für die Verarbeitung und Anzeige des UI-Status zuständig sein.

In diesem Abschnitt wird der unidirektionale Datenfluss (Unidirectional Data Flow, UDF) behandelt, ein Architekturmuster, das diese Trennung der Verantwortlichkeiten unterstützt.

State Holder

Statusinhaber sind die Klassen, die für die Erstellung des UI-Status und die Logik verantwortlich sind, die zum Erstellen dieses Status erforderlich ist. State-Holder gibt es in verschiedenen Größen, je nach Umfang der entsprechenden UI-Elemente, die sie verwalten. Das kann ein einzelnes Widget wie eine untere App-Leiste oder ein ganzer Bildschirm oder ein Navigationsziel sein.

Im letzteren Fall ist die typische Implementierung eine Instanz eines ViewModel. Je nach den Anforderungen der Anwendung kann jedoch auch eine einfache Klasse ausreichen. In der News-App aus der Fallstudie wird beispielsweise die Klasse NewsViewModel als Statusinhaber verwendet, um den UI-Status für den in diesem Abschnitt angezeigten Bildschirm zu erzeugen.

Es gibt viele Möglichkeiten, die Abhängigkeit zwischen der Benutzeroberfläche und dem zugehörigen State-Producer zu modellieren. Da die Interaktion zwischen der Benutzeroberfläche und ihrer ViewModel-Klasse jedoch weitgehend als Eingabe von Ereignissen und der daraus resultierenden Ausgabe von Status verstanden werden kann, lässt sich die Beziehung wie im folgenden Diagramm darstellen:

Anwendungsdaten fließen von der Datenschicht zum ViewModel. Der UI-Status wird vom ViewModel an die UI-Elemente weitergegeben und Ereignisse werden von den UI-Elementen zurück an das ViewModel gesendet.
Abbildung 4: Diagramm zur Funktionsweise von UDF in der App-Architektur.

Das Muster, bei dem der Status nach unten und die Ereignisse nach oben fließen, wird als unidirektionaler Datenfluss (Unidirectional Data Flow, UDF) bezeichnet. Die Auswirkungen dieses Musters auf die App-Architektur sind folgende:

  • Das ViewModel enthält und macht den Status verfügbar, der von der Benutzeroberfläche verwendet werden soll. Der UI-Status sind Anwendungsdaten, die vom ViewModel transformiert werden.
  • Die Benutzeroberfläche benachrichtigt das ViewModel über Nutzerereignisse.
  • Das ViewModel verarbeitet die Nutzeraktionen und aktualisiert den Status.
  • Der aktualisierte Status wird an die Benutzeroberfläche zurückgegeben, damit er gerendert werden kann.
  • Das oben beschriebene Verfahren wird für jedes Ereignis wiederholt, das eine Änderung des Status verursacht.

Für Navigationsziele oder ‑bildschirme ruft das ViewModel Daten aus Repositories oder Use-Case-Klassen ab und transformiert sie in den UI-Status. Dabei werden auch die Auswirkungen von Ereignissen berücksichtigt, die zu Änderungen des Status führen können. Die oben erwähnte Fallstudie enthält eine Liste von Artikeln mit jeweils einem Titel, einer Beschreibung, einer Quelle, einem Autorennamen, einem Erscheinungsdatum und der Information, ob der Artikel mit einem Lesezeichen versehen wurde. Die Benutzeroberfläche für die einzelnen Artikel sieht so aus:

Ein einzelner Artikel aus der Fallstudien-App. Die Benutzeroberfläche zeigt ein Thumbnail, den Titel des Artikels, den Autor, die geschätzte Lesezeit und ein Lesezeichen-Symbol an.
Abbildung 5. Benutzeroberfläche eines Artikel-Elements in der Fallstudien-App.

Wenn ein Nutzer einen Artikel mit einem Lesezeichen versehen möchte, ist das ein Beispiel für ein Ereignis, das Statusänderungen verursachen kann. Als State-Producer ist das ViewModel dafür verantwortlich, die gesamte Logik zu definieren, die zum Ausfüllen aller Felder im UI-Zustand und zum Verarbeiten der Ereignisse erforderlich ist, damit die Benutzeroberfläche vollständig gerendert werden kann.

Ein UI-Ereignis tritt auf, wenn der Nutzer einen Artikel mit einem Lesezeichen versieht. Das ViewModel benachrichtigt die Datenschicht über die Statusänderung. In der Datenschicht wird die Datenänderung beibehalten und die Anwendungsdaten werden aktualisiert. Die neuen App-Daten mit dem als Lesezeichen gespeicherten Artikel werden an das ViewModel übergeben, das dann den neuen UI-Zustand erzeugt und ihn zur Anzeige an die UI-Elemente übergibt.
Abbildung 6. Diagramm, das den Zyklus von Ereignissen und Daten in UDF veranschaulicht.

In den folgenden Abschnitten werden die Ereignisse, die Statusänderungen verursachen, und die Verarbeitung mit UDFs genauer betrachtet.

Arten von Logik

Das Setzen eines Lesezeichens für einen Artikel ist ein Beispiel für Geschäftslogik, da es einen Mehrwert für Ihre App bietet. Weitere Informationen finden Sie auf der Seite Datenebene. Es gibt jedoch verschiedene Arten von Logik, die definiert werden müssen:

  • Die Geschäftslogik ist die Implementierung von Produktanforderungen für App-Daten. Wie bereits erwähnt, ist ein Beispiel das Setzen eines Lesezeichens für einen Artikel in der Fallstudien-App. Die Geschäftslogik befindet sich in der Regel in den Domain- oder Datenschichten, aber niemals in der UI-Schicht.
  • UI-Verhaltenslogik oder UI-Logik beschreibt, wie Zustandsänderungen auf dem Bildschirm angezeigt werden. Beispiele hierfür sind das Abrufen des richtigen Texts für die Anzeige auf dem Bildschirm mit Android Resources, das Navigieren zu einem bestimmten Bildschirm, wenn der Nutzer auf eine Schaltfläche klickt, oder das Anzeigen einer Nutzermitteilung auf dem Bildschirm mit einem Toast oder einem Snackbar.

Die UI-Logik sollte in der UI und nicht im ViewModel enthalten sein, insbesondere wenn es sich um UI-Typen wie Context handelt. Wenn die Benutzeroberfläche komplexer wird und Sie die UI-Logik an eine andere Klasse delegieren möchten, um die Testbarkeit und die Trennung von Belangen zu verbessern, können Sie eine einfache Klasse als State Holder erstellen. Einfache Klassen, die in der Benutzeroberfläche erstellt werden, können Android SDK-Abhängigkeiten haben, da sie dem Lebenszyklus der Benutzeroberfläche folgen. ViewModel-Objekte haben eine längere Lebensdauer.

Weitere Informationen zu State-Holdern und dazu, wie sie beim Erstellen von Benutzeroberflächen helfen, finden Sie im Leitfaden zu Jetpack Compose State.

Vorteile von UDF

In der UDF wird der Zyklus der Statusproduktion wie in Abbildung 4 dargestellt. Außerdem wird zwischen dem Ort, an dem Zustandsänderungen entstehen, dem Ort, an dem sie transformiert werden, und dem Ort, an dem sie schließlich verwendet werden, unterschieden. Durch diese Trennung kann die Benutzeroberfläche genau das tun, was ihr Name impliziert: Informationen anzeigen, indem sie Statusänderungen beobachtet, und Nutzerabsichten weiterleiten, indem sie diese Änderungen an das ViewModel übergibt.

Mit anderen Worten: Mit benutzerdefinierten Funktionen ist Folgendes möglich:

  • Datenkonsistenz: Es gibt eine einzige Quelle für die Benutzeroberfläche.
  • Testbarkeit: Die Quelle des Status ist isoliert und kann daher unabhängig von der Benutzeroberfläche getestet werden.
  • Wartbarkeit: Die Änderung des Status folgt einem genau definierten Muster, bei dem Änderungen sowohl auf Nutzerereignisse als auch auf die Datenquellen zurückzuführen sind, aus denen Daten abgerufen werden.

UI-Status verfügbar machen

Nachdem Sie den UI-Status definiert und festgelegt haben, wie Sie die Erstellung dieses Status verwalten, besteht der nächste Schritt darin, den erstellten Status in der Benutzeroberfläche darzustellen.

Wenn Sie UDF verwenden, um die Produktion von Status zu verwalten, können Sie den erstellten Status als Stream betrachten. Das bedeutet, dass im Laufe der Zeit mehrere Versionen des Status erstellt werden. Stellen Sie den UI-Status in einem beobachtbaren Daten-Holder wie StateFlow bereit. So kann die Benutzeroberfläche auf Änderungen im Status reagieren, ohne dass Daten manuell direkt aus dem ViewModel abgerufen werden müssen. Das hat auch den Vorteil, dass immer die aktuelle Version des UI-Zustands im Cache gespeichert wird, was für die schnelle Wiederherstellung des Zustands nach Konfigurationsänderungen nützlich ist.

class NewsViewModel(
    // ...
) : ViewModel() {

    val uiState: NewsUiState = /* ... */
}

Eine Einführung in Kotlin-Flows finden Sie unter Kotlin-Flows auf Android. Informationen zur Verwendung von StateFlow als beobachtbarer Daten-Holder finden Sie im Codelab Erweiterte Konzepte für Zustand und Side Effects in Jetpack Compose.

Wenn die in der Benutzeroberfläche angezeigten Daten relativ einfach sind, lohnt es sich oft, sie in einen UI-Zustandstyp einzuschließen, da so die Beziehung zwischen der Ausgabe des Zustandshalters und dem zugehörigen Bildschirm oder UI-Element verdeutlicht wird. Wenn das UI-Element komplexer wird, lässt sich die Definition des UI-Zustands ganz einfach erweitern, sodass Sie die zusätzlichen Informationen berücksichtigen können, die zum Rendern des UI-Elements erforderlich sind.

Eine gängige Methode zum Erstellen eines Streams von UiState ist das Bereitstellen einer mutableStateOf-Eigenschaft mit einem private set. Der Status bleibt im ViewModel veränderbar, ist aber für die Benutzeroberfläche schreibgeschützt.

class NewsViewModel(
    // ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    // ...
}

Das ViewModel kann dann Methoden bereitstellen, die den Status intern ändern und Updates für die Benutzeroberfläche veröffentlichen. Nehmen wir beispielsweise an, Sie müssen eine asynchrone Aktion ausführen. Sie können eine Coroutine mit viewModelScope starten und den veränderlichen Status nach Abschluss aktualisieren.

class NewsViewModel(
    private val repository: NewsRepository,
    // ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

Im vorherigen Beispiel versucht die NewsViewModel-Klasse, Artikel für eine bestimmte Kategorie abzurufen, und gibt dann das Ergebnis des Versuchs – ob erfolgreich oder nicht – im UI-Zustand wieder, sodass die Benutzeroberfläche entsprechend reagieren kann. Weitere Informationen zur Fehlerbehandlung finden Sie im Abschnitt Fehler auf dem Bildschirm anzeigen.

Weitere Überlegungen

Zusätzlich zu den vorherigen Richtlinien sollten Sie beim Bereitstellen des UI-Zustands Folgendes beachten:

  • Verwenden Sie ein einzelnes UI-Zustandsobjekt, um zusammengehörige Zustände zu verarbeiten. Das führt zu weniger Inkonsistenzen und macht den Code leichter verständlich. Wenn Sie die Liste der Nachrichtenartikel und die Anzahl der Lesezeichen in zwei verschiedenen Streams bereitstellen, kann es passieren, dass einer aktualisiert wird, der andere aber nicht. Wenn Sie einen einzelnen Stream verwenden, werden beide Elemente auf dem neuesten Stand gehalten. Außerdem erfordert einige Geschäftslogik möglicherweise eine Kombination von Quellen. Sie müssen beispielsweise eine Lesezeichenschaltfläche nur dann anzeigen, wenn der Nutzer angemeldet und Abonnent eines Premium-Nachrichtendienstes ist. Sie können eine UI-Statusklasse so definieren:

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium

    In dieser Deklaration ist die Sichtbarkeit der Lesezeichenschaltfläche eine abgeleitete Property von zwei anderen Properties. Je komplexer die Geschäftslogik wird, desto wichtiger ist es, eine einzelne UiState-Klasse zu haben, in der alle Eigenschaften sofort verfügbar sind.

  • UI-Status: Einzelner Stream oder mehrere Streams? Der wichtigste Grundsatz bei der Entscheidung, ob der UI-Status in einem einzelnen Stream oder in mehreren Streams bereitgestellt werden soll, ist die Beziehung zwischen den ausgegebenen Elementen. Die größten Vorteile einer Single-Stream-Bereitstellung sind die Benutzerfreundlichkeit und die Datenkonsistenz: Nutzer des Status haben jederzeit Zugriff auf die neuesten Informationen. Es gibt jedoch Fälle, in denen separate Streams von Status aus dem ViewModel sinnvoll sein können:

    • Unabhängige Datentypen:Einige Status, die zum Rendern der Benutzeroberfläche erforderlich sind, sind möglicherweise völlig unabhängig voneinander. In solchen Fällen überwiegen die Kosten für die Zusammenfassung dieser unterschiedlichen Status möglicherweise die Vorteile, insbesondere wenn einer dieser Status häufiger aktualisiert wird als der andere.

    • UiState-Vergleich:Je mehr Felder ein UiState-Objekt enthält, desto wahrscheinlicher ist es, dass der Stream aufgrund der Aktualisierung eines seiner Felder ausgegeben wird. Da UI-Elemente keinen Differenzierungsmechanismus haben, um zu erkennen, ob aufeinanderfolgende Emissionen unterschiedlich oder gleich sind, führt jede Emission zu einer Aktualisierung des UI-Elements. Das bedeutet, dass möglicherweise Gegenmaßnahmen mit Flow-API-Methoden wie distinctUntilChanged() erforderlich sind.

Weitere Informationen zum Rendern und zum UI-Status finden Sie unter Lebenszyklus von Composables.

UI-Zustand verwenden

Wenn Sie den Stream von UiState-Objekten in der Benutzeroberfläche verwenden möchten, verwenden Sie den Terminaloperator für den Observable-Datentyp, den Sie verwenden. Verwenden Sie beispielsweise für Kotlin-Flows die Methode collect() oder ihre Varianten.

Wenn Sie beobachtbare Daten-Holder in der Benutzeroberfläche verwenden, müssen Sie den Lebenszyklus der Benutzeroberfläche berücksichtigen. Die Benutzeroberfläche soll den UI-Status nicht beobachten, wenn das Composable dem Nutzer nicht angezeigt wird. Weitere Informationen zu diesem Thema finden Sie in diesem Blogpost. Wenn Sie Flows verwenden, sollten Sie Lifecycle-Probleme am besten mit dem entsprechenden Coroutine-Bereich und der collectAsStateWithLifecycle API beheben:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

Laufende Vorgänge anzeigen

Eine einfache Möglichkeit, Ladestatus in einer UiState-Klasse darzustellen, ist ein boolesches Feld:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    // ...
)

Der Wert dieses Flags gibt an, ob in der Benutzeroberfläche eine Fortschrittsanzeige vorhanden ist.

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

Fehler auf dem Display anzeigen

Die Anzeige von Fehlern in der Benutzeroberfläche ähnelt der Anzeige von laufenden Vorgängen, da beide einfach durch boolesche Werte dargestellt werden können, die ihr Vorhandensein oder Fehlen angeben. Fehler können aber auch eine zugehörige Nachricht enthalten, die an den Nutzer zurückgegeben wird, oder eine zugehörige Aktion, mit der der fehlgeschlagene Vorgang noch einmal versucht wird. Daher müssen Fehlerstatus, während ein laufender Vorgang entweder geladen wird oder nicht, möglicherweise mit Datenklassen modelliert werden, die die für den Kontext des Fehlers geeigneten Metadaten enthalten.

Sehen Sie sich das vorherige Beispiel an, in dem beim Abrufen von Artikeln ein Fortschrittsbalken angezeigt wurde. Wenn bei diesem Vorgang ein Fehler auftritt, sollten Sie dem Nutzer möglicherweise eine oder mehrere Meldungen anzeigen, in denen die Ursache des Fehlers erläutert wird.

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    // ...
)

Sie können die Fehlermeldungen dann in Form von UI-Elementen wie Infoleisten für den Nutzer anzeigen. Weitere Informationen dazu, wie UI-Ereignisse erzeugt und genutzt werden, finden Sie unter UI-Ereignisse.

Threading und Nebenläufigkeit

Achten Sie darauf, dass alle in einem ViewModel ausgeführten Vorgänge main-safe sind, d. h., sie können sicher vom Hauptthread aus aufgerufen werden. Die Daten- und Domänenebenen sind dafür verantwortlich, dass die Arbeit in einen anderen Thread verschoben wird.

Wenn ein ViewModel zeitaufwendige Vorgänge ausführt, ist es auch dafür verantwortlich, diese Logik in einen Hintergrundthread zu verschieben. Kotlin-Coroutinen sind eine gute Möglichkeit, gleichzeitige Vorgänge zu verwalten, und die Jetpack-Architekturkomponenten bieten integrierte Unterstützung dafür. Weitere Informationen zur Verwendung von Koroutinen in Android-Apps finden Sie unter Kotlin-Koroutinen in Android.

Änderungen in der App-Navigation werden oft durch ereignisähnliche Emissionen ausgelöst. Wenn sich beispielsweise eine SignInViewModel-Klasse anmeldet, kann für das UiState-Objekt das Feld isSignedIn auf true festgelegt sein. Verwenden Sie diese Trigger genauso wie die im vorherigen Abschnitt UI-Status verwenden beschriebenen, aber verschieben Sie die Implementierung der Verwendung auf die Navigationskomponente.

Weitere Informationen zur Navigation auf der Benutzeroberfläche findest du unter Navigation 3.

Paging

Die Paging Library wird in der Benutzeroberfläche mit einem Typ namens PagingData verwendet. Da PagingData Elemente darstellt und enthält, die sich im Laufe der Zeit ändern können – es ist also kein unveränderlicher Typ –, sollte es nicht in einem unveränderlichen UI-Zustand dargestellt werden. Stattdessen sollte sie unabhängig vom ViewModel in einem eigenen Stream verfügbar gemacht werden.

Das folgende Beispiel zeigt die Compose API der Paging-Bibliothek:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

Animationen

Damit die Übergänge in der Navigation auf oberster Ebene reibungslos ablaufen, sollten Sie warten, bis auf dem zweiten Bildschirm Daten geladen wurden, bevor Sie die Animation starten.

Weitere Informationen zu Navigationsübergängen finden Sie unter Navigation 3 und Übergänge für gemeinsame Elemente in Compose.

Zusätzliche Ressourcen

Inhalte ansehen

Beispiele

In den folgenden Google-Beispielen wird die Verwendung der UI-Ebene veranschaulicht. Sehen Sie sich die folgenden Beispiele an, um zu sehen, wie diese Anleitung in der Praxis aussieht: