UI-Ebene

Die Benutzeroberfläche besteht darin, die Anwendungsdaten auf dem Bildschirm anzuzeigen und als primärer Punkt für die Nutzerinteraktion zu dienen. Immer wenn sich die Daten aufgrund einer Nutzerinteraktion (z. B. Drücken einer Schaltfläche) oder einer externen Eingabe (z. B. einer Netzwerkantwort) ändern, sollte die Benutzeroberfläche entsprechend aktualisiert werden. Die UI ist eine visuelle Darstellung des aus der Datenschicht abgerufenen Anwendungsstatus.

Die Anwendungsdaten, die Sie aus der Datenschicht erhalten, haben jedoch normalerweise ein anderes Format als die anzuzeigenden Informationen. Beispielsweise kann es sein, dass Sie nur einen Teil der Daten für die Benutzeroberfläche benötigen oder zwei verschiedene Datenquellen zusammenführen müssen, um für den Nutzer relevante Informationen anzuzeigen. Unabhängig von der angewendeten Logik müssen der UI alle Informationen übergeben werden, die zum vollständigen Rendern erforderlich sind. Die UI-Ebene ist die Pipeline, die Änderungen von Anwendungsdaten in ein Formular umwandelt, das in der UI angezeigt werden kann, und zeigt sie dann an.

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

Eine einfache Fallstudie

Nehmen wir als Beispiel eine App, die Nachrichtenartikel abruft, damit Nutzer sie lesen können. Die App hat einen Artikelbildschirm, auf dem Artikel präsentiert werden, die zum Lesen verfügbar sind. Außerdem können angemeldete Nutzer Artikel als Lesezeichen speichern, die besonders auffallen. Da es zu einem bestimmten Zeitpunkt viele Artikel geben kann, sollte der Leser die Artikel nach Kategorie durchsuchen können. Zusammenfassend lässt sich sagen, dass Nutzer mit der App Folgendes tun können:

  • Hier finden Sie Artikel, die gelesen werden können.
  • Artikel nach Kategorie durchsuchen
  • Melden Sie sich an und speichern Sie bestimmte Artikel als Lesezeichen.
  • Du erhältst Zugriff auf einige Premium-Funktionen, sofern du berechtigt bist.
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 Anwendungsarchitektur für die UI-Ebene lösen.

Architektur der UI-Ebene

Der Begriff UI bezieht sich auf UI-Elemente wie Aktivitäten und Fragmente, die die Daten anzeigen, unabhängig von den dafür verwendeten APIs (Views oder Jetpack Compose). Da die Datenschicht darin besteht, die App-Daten zu speichern, zu verwalten und Zugriff darauf zu gewähren, muss die UI-Ebene die folgenden Schritte ausführen:

  1. App-Daten verarbeiten und in Daten umwandeln, die die Benutzeroberfläche leicht rendern kann
  2. Über die UI gerenderte Daten verarbeiten und in UI-Elemente zur Präsentation dem Nutzer umwandeln
  3. Nutzereingabeereignisse aus diesen zusammengestellten UI-Elementen verarbeiten und ihre Auswirkungen in den UI-Daten nach Bedarf widerspiegeln
  4. Wiederholen Sie die Schritte 1 bis 3 so lange wie nötig.

Im weiteren Verlauf dieses Leitfadens wird gezeigt, wie Sie eine UI-Ebene implementieren, auf der diese Schritte ausgeführt werden. In diesem Leitfaden werden insbesondere die folgenden Aufgaben und Konzepte behandelt:

  • So definieren Sie den UI-Status.
  • Unidirektionaler Datenfluss (UDF) zum Erstellen und Verwalten des UI-Status.
  • Anleitung zum Bereitstellen des UI-Status mit beobachtbaren Datentypen gemäß UDF-Prinzipien.
  • Anleitung zum Implementieren einer UI, die den beobachtbaren UI-Status nutzt.

Das grundlegendste ist die Definition des UI-Status.

UI-Status definieren

Weitere Informationen finden Sie in der zuvor beschriebenen Fallstudie. Die Benutzeroberfläche enthält also eine Liste von Artikeln und Metadaten für jeden Artikel. Bei diesen Informationen, die die App dem Nutzer anzeigt, handelt es sich um den Status der Benutzeroberfläche.

Mit anderen Worten: Wenn die UI das ist, was der Nutzer sieht, ist der UI-Status das, was die App laut der App sehen sollte. Wie zwei Seiten derselben Münze ist die UI die visuelle Darstellung des UI-Status. Änderungen am Status der Benutzeroberfläche werden sofort in der UI angezeigt.

„UI“ ist das Ergebnis der Verknüpfung von UI-Elementen auf dem Bildschirm mit dem UI-Status.
Abbildung 3. „UI“ ist das Ergebnis der Verknüpfung von UI-Elementen auf dem Bildschirm mit dem UI-Status.

Betrachten Sie die Fallstudie. Um die Anforderungen der Nachrichten-App zu erfüllen, können die Informationen, die zum vollständigen Rendern der UI erforderlich sind, in eine 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,
    ...
)

Unveränderlichkeit

Die UI-Statusdefinition im obigen Beispiel ist unveränderlich. Der Hauptvorteil besteht darin, dass unveränderliche Objekte Garantien in Bezug auf den Zustand der Anwendung zu einem bestimmten Zeitpunkt bieten. Dadurch kann sich die UI auf eine einzelne Rolle konzentrieren: den Status lesen und seine UI-Elemente entsprechend aktualisieren. Daher sollten Sie den UI-Status in der UI nur dann direkt ändern, wenn die UI selbst die einzige Quelle für ihre Daten ist. Ein Verstoß gegen dieses Prinzip führt zu mehreren Datenquellen für dieselbe Information, was zu Dateninkonsistenzen und subtilen Fehlern führt.

Wenn beispielsweise das Flag bookmarked in einem NewsItemUiState-Objekt aus dem UI-Status in der Fallstudie in der Klasse Activity aktualisiert wird, konkurriert dieses Flag mit der Datenschicht als Quelle des Lesezeichenstatus eines Artikels. Unveränderliche Datenklassen sind sehr nützlich, um diese Art von Antimustern zu verhindern.

Namenskonventionen in diesem Leitfaden

In dieser Anleitung werden UI-Zustandsklassen basierend auf der Funktionalität des Bildschirms oder Teil des Bildschirms benannt, die sie beschreiben. Dafür gilt folgende Konvention:

functionality + UiState.

Der Status eines Bildschirms, auf dem Nachrichten angezeigt werden, kann beispielsweise NewsUiState und der Status eines Nachrichtenelements in einer Liste von Nachrichtenelementen ein NewsItemUiState sein.

Status mit unidirektionalem Datenfluss verwalten

Im vorherigen Abschnitt wurde festgelegt, dass der UI-Status ein unveränderlicher Snapshot der Details ist, die zum Rendern der UI erforderlich sind. Da Daten in Apps jedoch dynamisch sind, kann sich dieser Status im Laufe der Zeit ändern. Dies kann auf Nutzerinteraktionen oder andere Ereignisse zurückzuführen sein, durch die die zugrunde liegenden Daten zum Befüllen der Anwendung geändert werden.

Diese Interaktionen können von einem Vermittler profitieren, der sie verarbeitet. Er definiert die Logik, die auf jedes Ereignis angewendet werden soll, und führt die erforderlichen Transformationen in den unterstützenden Datenquellen durch, um den UI-Status zu erstellen. Diese Interaktionen und ihre Logik können sich in der Benutzeroberfläche selbst befinden, aber dies kann schnell unhandlich werden, da die Benutzeroberfläche anfänglich mehr wird, als der Name schon sagt: Sie wird zu Dateninhaber, Ersteller, Transformer und mehr. Darüber hinaus kann dies die Testbarkeit beeinträchtigen, da der resultierende Code ein eng gekoppeltes Amalgam ohne erkennbare Grenzen ist. Letztlich wird die Benutzeroberfläche von einem geringeren Aufwand profitieren. Sofern der UI-Status nicht sehr einfach ist, sollte die UI allein dafür verantwortlich sein, den UI-Status zu verarbeiten und anzuzeigen.

In diesem Abschnitt wird der unidirektionale Datenfluss (Unidirektional Data Flow, UDF) erläutert, ein Architekturmuster, mit dem diese fehlerfreie Verantwortungstrennung durchgesetzt werden kann.

Inhaber des Bundesstaates

Die Klassen, die für die Erzeugung des UI-Status verantwortlich sind und die erforderliche Logik für diese Aufgabe enthalten, werden als Statusinhaber bezeichnet. Statusinhaber sind in verschiedenen Größen erhältlich, abhängig vom Umfang der entsprechenden UI-Elemente, die sie verwalten, von einem einzelnen Widget wie einer unteren App-Leiste bis hin zu einem ganzen Bildschirm oder einem Navigationsziel.

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

Es gibt viele Möglichkeiten, die Codeabhängigkeit zwischen der UI und ihrem Zustandsersteller zu modellieren. Da die Interaktion zwischen der UI und ihrer ViewModel-Klasse jedoch weitgehend als Ereignis input und als Folgestatus output verstanden werden kann, lässt sich die Beziehung wie im folgenden Diagramm dargestellt:

Anwendungsdaten fließen von der Datenschicht zur ViewModel. UI-Zustandsdatenfluss von ViewModel zu den UI-Elementen und Ereignisse von den UI-Elementen zurück zu ViewModel.
Abbildung 4: Diagramm, wie UDF in der Anwendungsarchitektur funktioniert.

Das Muster, in dem der Zustand nach unten und die Ereignisse nach oben fließen, wird als unidirektionaler Datenfluss (unidirektionale Datenfluss) bezeichnet. Dieses Muster hat folgende Auswirkungen auf die Anwendungsarchitektur:

  • Das ViewModel enthält den Status, der von der UI verarbeitet werden soll, und stellt ihn bereit. Der UI-Status besteht aus Anwendungsdaten, die von ViewModel transformiert wurden.
  • Die UI benachrichtigt das ViewModel über Nutzerereignisse.
  • Das ViewModel übernimmt die Nutzeraktionen und aktualisiert den Status.
  • Der aktualisierte Status wird zum Rendern an die UI zurückgegeben.
  • Dies wird für jedes Ereignis wiederholt, das eine Statusänderung verursacht.

Bei Navigationszielen oder -bildschirmen arbeitet ViewModel mit Repositories oder Anwendungsfallklassen, um Daten abzurufen und in den UI-Zustand umzuwandeln und dabei die Auswirkungen von Ereignissen zu berücksichtigen, die Mutationen des Status verursachen können. Die zuvor erwähnte Fallstudie enthält eine Liste von Artikeln mit Titel, Beschreibung, Quelle, Name des Autors, Erscheinungsdatum und Angabe, ob ein Lesezeichen gespeichert wurde. Die Benutzeroberfläche für jedes Artikelelement sieht so aus:

Abbildung 5. Benutzeroberfläche eines Artikelelements in der Fallstudien-App

Ein Nutzer, der ein Lesezeichen für einen Artikel anfordert, ist ein Beispiel für ein Ereignis, das zu Statusmutationen führen kann. Als Zustandsersteller ist die ViewModel dafür verantwortlich, die erforderliche Logik zum Ausfüllen aller Felder im UI-Status zu definieren und die Ereignisse zu verarbeiten, die für das vollständige Rendern der UI erforderlich sind.

Ein UI-Ereignis tritt auf, wenn der Nutzer ein Lesezeichen für einen Artikel speichert. ViewModel benachrichtigt die Datenschicht über die Statusänderung. Die Datenschicht behält die Datenänderung bei und aktualisiert die Anwendungsdaten. Die neuen App-Daten mit dem als Lesezeichen gespeicherten Artikel werden an das ViewModel übergeben, das dann den neuen UI-Status erzeugt und zur Anzeige an die UI-Elemente übergibt.
Abbildung 6. Diagramm, das den Zyklus von Ereignissen und Daten in UDF veranschaulicht.

In den folgenden Abschnitten wird näher auf die Ereignisse eingegangen, die zu Statusänderungen führen, und wie sie mit UDF verarbeitet werden können.

Logiktypen

Das Speichern eines Artikels als Lesezeichen ist ein Beispiel für Geschäftslogik, weil dies Ihrer Anwendung einen Mehrwert bietet. Weitere Informationen dazu finden Sie auf der Seite Datenschicht. Es gibt jedoch verschiedene Arten von Logik, die wichtig zu definieren sind:

  • Die Geschäftslogik ist die Implementierung von Produktanforderungen an Anwendungsdaten. Wie bereits erwähnt, ist ein Beispiel das Speichern eines Artikels in der Fallstudien-App als Lesezeichen. Die Geschäftslogik wird normalerweise in der Domain- oder Datenebene platziert, jedoch nie auf der UI-Ebene.
  • Die UI-Verhaltenslogik oder UI-Logik ist wie Statusänderungen auf dem Bildschirm angezeigt werden. Beispiele hierfür sind das Abrufen des richtigen Textes, der auf dem Bildschirm angezeigt werden soll, indem Android Resources verwendet wird, das Aufrufen eines bestimmten Bildschirms, wenn der Nutzer auf eine Schaltfläche klickt, oder das Einblenden einer Nutzernachricht auf dem Bildschirm mithilfe eines Toasts oder einer Snackbar.

Die UI-Logik, insbesondere wenn sie UI-Typen wie Context umfasst, sollte in der UI und nicht in der ViewModel vorhanden sein. Wenn die Komplexität der UI zunimmt und Sie die UI-Logik an eine andere Klasse delegieren möchten, um die Testbarkeit und die Trennung von Bedenken zu bevorzugen, können Sie eine einfache Klasse als Zustandsinhaber erstellen. Einfache Klassen, die in der Benutzeroberfläche erstellt wurden, können Android SDK-Abhängigkeiten übernehmen, da sie dem Lebenszyklus der Benutzeroberfläche folgen. ViewModel-Objekte haben eine längere Lebensdauer.

Weitere Informationen zu Inhabern von Bundesstaaten und deren Rolle in der Erstellung von UIs finden Sie im Jetpack Compose State-Leitfaden.

Warum UDF verwenden?

UDF modelliert den Zyklus der Zustandsproduktion, wie in Abbildung 4 dargestellt. Es trennt auch den Ort, an dem Statusänderungen auftreten, den Ort, an dem sie transformiert werden, und den Ort, an dem sie schließlich ausgeführt werden. Durch diese Trennung kann die UI genau das tun, was ihr Name schon sagt: Informationen durch Beobachtung von Statusänderungen anzeigen und Nutzerabsichten weiterleiten, indem diese Änderungen an ViewModel übergeben werden.

Mit anderen Worten, UDF ermöglicht Folgendes:

  • Datenkonsistenz: Es gibt eine zentrale, verlässliche Datenquelle für die Benutzeroberfläche.
  • Testbarkeit: Die Zustandsquelle ist isoliert und daher unabhängig von der UI testbar.
  • Wartungsfreundlichkeit. Die Status-Mutation folgt einem klar definierten Muster, bei dem Mutationen sowohl das Ergebnis von Nutzerereignissen als auch den Datenquellen sind, aus denen sie abgerufen werden.

UI-Status freigeben

Nachdem Sie den UI-Status definiert und festgelegt haben, wie Sie die Produktion dieses Zustands verwalten, besteht der nächste Schritt darin, der UI den erzeugten Status zu präsentieren. Da Sie UDF zur Verwaltung der Zustandserzeugung verwenden, können Sie den erzeugten Zustand als Stream betrachten. Mit anderen Worten, es werden im Laufe der Zeit mehrere Versionen des Status erzeugt. Daher sollten Sie den UI-Status in einem beobachtbaren Datenbehälter wie LiveData oder StateFlow bereitstellen. Der Grund dafür ist, dass die UI auf alle Änderungen am Status reagieren kann, ohne Daten manuell direkt aus ViewModel abrufen zu müssen. Diese Typen haben außerdem den Vorteil, dass immer die neueste Version des UI-Status im Cache gespeichert wird. Dies ist nützlich, um den Status nach Konfigurationsänderungen schnell wiederherzustellen.

Aufrufe

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

    val uiState: StateFlow<NewsUiState> = …
}

Schreiben

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

    val uiState: NewsUiState = …
}

Eine Einführung in LiveData als beobachtbare Dateninhaber finden Sie in diesem Codelab. Eine ähnliche Einführung in Kotlin-Abläufe finden Sie unter Kotlin-Abläufe unter Android.

In Fällen, in denen die für die Benutzeroberfläche freigegebenen Daten relativ einfach sind, lohnt es sich oft, die Daten in einen UI-Zustandstyp zu verpacken, da er die Beziehung zwischen den Emissionen des Staatsinhabers und dem zugehörigen Bildschirm oder UI-Element vermittelt. Mit zunehmender Komplexität des UI-Elements ist es außerdem immer einfacher, die Definition des UI-Status zu ergänzen, um die zusätzlichen Informationen aufzunehmen, die zum Rendern des UI-Elements erforderlich sind.

Eine gängige Methode zum Erstellen eines UiState-Streams besteht darin, einen änderbaren Back-End-Stream als unveränderlichen Stream aus ViewModel bereitzustellen, z. B. einen MutableStateFlow<UiState> als StateFlow<UiState>.

Aufrufe

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

Schreiben

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

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

Die ViewModel kann dann Methoden zur Verfügung stellen, die den Status intern ändern und Aktualisierungen für die UI veröffentlichen. Angenommen, eine asynchrone Aktion muss ausgeführt werden. Eine Koroutine kann mit viewModelScope gestartet und der änderbare Status nach Abschluss aktualisiert werden.

Aufrufe

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

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

Schreiben

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 obigen Beispiel versucht die NewsViewModel-Klasse, Artikel für eine bestimmte Kategorie abzurufen, und spiegelt dann das Ergebnis des Versuchs – ob Erfolg oder Misserfolg – in dem UI-Status wider, in dem die UI entsprechend darauf reagieren kann. Weitere Informationen zur Fehlerbehandlung finden Sie im Abschnitt Fehler auf dem Bildschirm anzeigen.

Weitere Überlegungen

Zusätzlich zur vorherigen Anleitung sollten Sie beim Bereitstellen des UI-Status Folgendes beachten:

  • Ein UI-Zustandsobjekt sollte Status verarbeiten, die miteinander in Beziehung stehen. Dies führt zu weniger Inkonsistenzen und macht den Code leichter verständlich. Wenn Sie die Liste der Nachrichtenelemente und die Anzahl der Lesezeichen in zwei verschiedenen Streams anzeigen, kann es vorkommen, dass ein Stream aktualisiert wurde und der andere nicht. Wenn Sie einen einzelnen Stream verwenden, werden beide Elemente aktualisiert. Darüber hinaus kann für manche Geschäftslogik eine Kombination von Quellen erforderlich sein. Beispielsweise kann es sein, dass eine Lesezeichenschaltfläche nur dann angezeigt werden muss, wenn der Nutzer angemeldet ist und Abonnent eines Premium-Nachrichtendienstes ist. So könnten Sie eine UI-Zustandsklasse 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 Lesezeichen-Schaltfläche eine abgeleitete Eigenschaft von zwei anderen Eigenschaften. Da die Geschäftslogik immer komplexer wird, wird es immer wichtiger, eine einzige UiState-Klasse zu haben, in der alle Attribute sofort verfügbar sind.

  • Benutzeroberfläche: einzelner Stream oder mehrere Streams? Das wichtigste Leitprinzip für die Auswahl zwischen der Anzeige des UI-Status in einem einzelnen Stream oder in mehreren Streams ist der vorherige Aufzählungspunkt: die Beziehung zwischen den ausgegebenen Elementen. Der größte Vorteil einer Präsenz in einem einzelnen Stream ist Bequemlichkeit und Datenkonsistenz: Staatliche Verbraucher haben jederzeit die neuesten Informationen verfügbar. Es gibt jedoch Instanzen, in denen separate Statusstreams von ViewModel geeignet sind:

    • Nicht zusammenhängende Datentypen:Einige Status, die zum Rendern der UI erforderlich sind, sind möglicherweise völlig unabhängig voneinander. In solchen Fällen können die Kosten für die Bündelung dieser unterschiedlichen Bundesstaaten die Vorteile überwiegen, insbesondere wenn einer dieser Bundesstaaten häufiger als der andere aktualisiert wird.

    • UiState diffing: Je mehr Felder ein UiState-Objekt enthält, desto wahrscheinlicher ist es, dass der Stream ausgegeben wird, wenn eines seiner Felder aktualisiert wird. Da bei Aufrufen kein unterschiedlicher Mechanismus verwendet wird, um nachzuvollziehen, ob aufeinanderfolgende Emissionen unterschiedlich oder gleich sind, führt jede Emissionen zu einer Aktualisierung der Ansicht. Daher ist möglicherweise eine Risikominderung mithilfe der Flow APIs oder Methoden wie distinctUntilChanged() auf der LiveData erforderlich.

UI-Status nutzen

Um den Stream von UiState-Objekten in der UI zu verarbeiten, verwenden Sie den Terminaloperator für den von Ihnen verwendeten beobachtbaren Datentyp. Für LiveData verwenden Sie beispielsweise die Methode observe() und für Kotlin-Abläufe die Methode collect() oder ihre Varianten.

Wenn Sie beobachtbare Dateninhaber in der UI verwenden, müssen Sie den Lebenszyklus der UI berücksichtigen. Das ist wichtig, weil die UI den Status der Benutzeroberfläche nicht beobachten sollte, wenn die Ansicht dem Nutzer nicht angezeigt wird. Weitere Informationen zu diesem Thema finden Sie in diesem Blogpost. Bei Verwendung von LiveData kümmert sich LifecycleOwner implizit um Fragen zum Lebenszyklus. Bei der Verwendung von Abläufen empfiehlt es sich, dies mit dem entsprechenden Koroutinenbereich und der repeatOnLifecycle API zu handhaben:

Aufrufe

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

Schreiben

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

Laufende Vorgänge anzeigen

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

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

Der Wert dieses Flags gibt an, ob auf der UI eine Fortschrittsanzeige vorhanden ist oder fehlt.

Aufrufe

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

Schreiben

@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 Bildschirm anzeigen

Die Anzeige von Fehlern auf der Benutzeroberfläche ähnelt der Anzeige laufender Vorgänge, da sie beide leicht durch boolesche Werte dargestellt werden können, um ihr Vorhandensein oder Abwesenheit anzuzeigen. Fehler können jedoch auch eine zugehörige Nachricht umfassen, die an den Nutzer weitergeleitet werden soll, oder eine mit ihnen verknüpfte Aktion, mit der der fehlgeschlagene Vorgang wiederholt wird. Während ein laufender Vorgang entweder geladen oder nicht geladen wird, müssen Fehlerstatus möglicherweise mit Datenklassen modelliert werden, die die Metadaten hosten, die für den Kontext des Fehlers geeignet sind.

Nehmen wir zum Beispiel das Beispiel aus dem vorherigen Abschnitt, bei dem beim Abrufen von Artikeln ein Fortschrittsbalken angezeigt wurde. Wenn dieser Vorgang zu einem Fehler führt, können Sie dem Nutzer eine oder mehrere Meldungen mit detaillierten Angaben zum Fehler anzeigen lassen.

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

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

Die Fehlermeldungen können dann dem Nutzer in Form von UI-Elementen wie Snackbars präsentiert werden. Da dies mit der Erstellung und Nutzung von UI-Ereignissen zusammenhängt, finden Sie auf der Seite UI-Ereignisse weitere Informationen.

Threading und Nebenläufigkeit

Alle Arbeiten, die in einer ViewModel ausgeführt werden, sollten main-safe sein, d. h., sie können sicher aus dem Hauptthread aufgerufen werden. Dies liegt daran, dass die Daten- und Domainebenen dafür verantwortlich sind, die Arbeit in einen anderen Thread zu verschieben.

Wenn ein ViewModel Vorgänge mit langer Ausführungszeit ausführt, muss es auch diese Logik in einen Hintergrundthread verschieben. Kotlin-Koroutinen sind eine hervorragende Möglichkeit, gleichzeitige Vorgänge zu verwalten. Die Jetpack-Architekturkomponenten bieten integrierte Unterstützung dafür. Weitere Informationen zur Verwendung von Koroutinen in Android-Apps finden Sie unter Kotlin-Koroutinen unter Android.

Veränderungen in der App-Navigation gehen oft auf ereignisbasierte Emissionen zurück. Nachdem beispielsweise eine SignInViewModel-Klasse eine Anmeldung durchgeführt hat, kann das Feld isSignedIn für UiState auf true gesetzt sein. Solche Trigger sollten wie die im obigen Abschnitt Consume UI-Status behandelten Trigger genutzt werden, mit der Ausnahme, dass sich die Implementierung des Verbrauchs auf die Navigationskomponente konzentrieren sollte.

Seitenumbruch

Die Paging-Bibliothek wird in der UI mit dem Typ 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 sollten Sie es unabhängig von ViewModel in einem eigenen Stream bereitstellen. Ein konkretes Beispiel hierfür finden Sie im Codelab zu Android Paging.

Animationen

Damit die Navigation auf oberster Ebene reibungslos verläuft, sollten Sie mit der Animation warten, bis die Daten auf dem zweiten Bildschirm geladen wurden. Das Android View-Framework bietet Hooks, um Übergänge zwischen Fragmentzielen mit den APIs postponeEnterTransition() und startPostponedEnterTransition() zu verzögern. Mit diesen APIs kann dafür gesorgt werden, dass die UI-Elemente auf dem zweiten Bildschirm (in der Regel ein aus dem Netzwerk abgerufenes Bild) bereit zur Anzeige sind, bevor die UI den Übergang zu diesem Bildschirm animiert. Weitere Informationen und Details zur Implementierung findest du im Android Motion-Beispiel.

Produktproben

Die folgenden Google-Beispiele zeigen die Verwendung der UI-Ebene. Sehen Sie sich diese Tipps in der Praxis an: