Denken in Textform

Jetpack Compose ist ein modernes deklaratives UI-Toolkit für Android. Mit Compose wird das Schreiben und Verwalten Ihrer Anwendungs-UI vereinfacht. Dazu wird eine deklarative API bereitgestellt, mit der Sie die Anwendungs-UI rendern können, ohne die Front-End-Ansichten zwingend zu ändern. Diese Terminologie muss erklärt werden, aber die Auswirkungen sind wichtig für Ihr Anwendungsdesign.

Das deklarative Programmierparadigma

In der Vergangenheit wurde eine Android-Ansichtshierarchie als Baum von UI-Widgets dargestellt. Wenn sich der Status der Anwendung aufgrund von Dingen wie Nutzerinteraktionen ändert, muss die UI-Hierarchie aktualisiert werden, um die aktuellen Daten anzuzeigen. Die gängigste Methode zum Aktualisieren der UI besteht darin, die Baumstruktur mit Funktionen wie findViewById() durchzugehen und Knoten durch Aufrufen von Methoden wie button.setText(String), container.addChild(View) oder img.setImageBitmap(Bitmap) zu ändern. Diese Methoden ändern den internen Status des Widgets.

Wenn Sie Ansichten manuell bearbeiten, steigt die Fehlerwahrscheinlichkeit. Wenn ein Datenelement an mehreren Stellen gerendert wird, vergisst man leicht, eine der Ansichten zu aktualisieren, in denen es angezeigt wird. Es ist auch einfach, illegale Staaten zu erstellen, wenn zwei Updates auf unerwartete Weise miteinander in Konflikt stehen. Bei einem Update kann beispielsweise versucht werden, den Wert eines Knotens festzulegen, der gerade aus der UI entfernt wurde. Im Allgemeinen erhöht sich die Komplexität der Softwarewartung mit der Anzahl der Ansichten, die aktualisiert werden müssen.

In den letzten Jahren hat die gesamte Branche mit der Umstellung auf ein deklaratives UI-Modell begonnen, was die Entwicklung und Aktualisierung von Benutzeroberflächen erheblich vereinfacht. Dabei wird konzeptionell der gesamte Bildschirm von Grund auf neu generiert und dann werden nur die notwendigen Änderungen angewendet. Bei diesem Ansatz entfällt die Komplexität der manuellen Aktualisierung einer Hierarchie für zustandsorientierte Ansichten. Compose ist ein deklaratives UI-Framework.

Eine Herausforderung bei der Neugenerierung des gesamten Bildschirms besteht darin, dass er in Bezug auf Zeit, Rechenleistung und Akkunutzung potenziell teuer ist. Um diese Kosten zu minimieren, wählt Compose intelligent aus, welche Teile der UI zu einem bestimmten Zeitpunkt neu gezeichnet werden müssen. Dies hat einige Auswirkungen auf die Gestaltung der UI-Komponenten, wie unter Neuzusammensetzung erläutert.

Eine einfache zusammensetzbare Funktion

Mit Compose können Sie eine Benutzeroberfläche erstellen, indem Sie eine Reihe von zusammensetzbaren Funktionen definieren, die Daten aufnehmen und UI-Elemente ausgeben. Ein einfaches Beispiel ist ein Greeting-Widget, das ein String annimmt und ein Text-Widget ausgibt, das eine Begrüßungsnachricht anzeigt.

Screenshot von einem Smartphone mit dem Text

Abbildung 1: Einfache zusammensetzbare Funktion, bei der Daten übergeben und zum Rendern eines Textwidgets auf dem Bildschirm verwendet werden

Einige Hinweise zu dieser Funktion:

  • Die Funktion wird mit der Annotation @Composable gekennzeichnet. Alle zusammensetzbaren Funktionen müssen diese Annotation haben. Sie informiert den Compose-Compiler darüber, dass mit dieser Funktion Daten in eine UI konvertiert werden sollen.

  • Die Funktion übernimmt Daten. Zusammensetzbare Funktionen können Parameter akzeptieren, mit denen die Anwendungslogik die UI beschreiben kann. In diesem Fall akzeptiert unser Widget eine String, damit der Nutzer mit seinem Namen begrüßt werden kann.

  • Die Funktion zeigt Text auf der Benutzeroberfläche an. Dazu wird die zusammensetzbare Funktion Text() aufgerufen, die das Text-UI-Element erstellt. Zusammensetzbare Funktionen geben eine UI-Hierarchie aus, indem sie andere zusammensetzbare Funktionen aufrufen.

  • Die Funktion gibt nichts zurück. Zusammensetzungsfunktionen, die UI ausgeben, müssen nichts zurückgeben, da sie den gewünschten Bildschirmstatus beschreiben, anstatt UI-Widgets zu erstellen.

  • Diese Funktion ist schnell, idempotent und frei von Nebeneffekten.

    • Die Funktion verhält sich gleich, wenn sie mehrmals mit demselben Argument aufgerufen wird. Sie verwendet keine anderen Werte wie globale Variablen oder Aufrufe von random().
    • Die Funktion beschreibt die UI ohne Nebeneffekte wie das Ändern von Eigenschaften oder globalen Variablen.

    Im Allgemeinen sollten alle zusammensetzbaren Funktionen mit diesen Attributen geschrieben werden. Gründe, die unter Neuzusammensetzung beschrieben werden.

Der deklarative Paradigmenwechsel

Bei vielen imperativen objektorientierten UI-Toolkits initialisieren Sie die UI, indem Sie eine Struktur mit Widgets instanziieren. Sie tun dies oft, indem Sie eine XML-Layoutdatei aufblähen. Jedes Widget behält seinen eigenen internen Status und stellt Getter- und Setter-Methoden bereit, mit denen die Anwendungslogik mit dem Widget interagieren kann.

Beim deklarativen Ansatz von Compose sind Widgets relativ zustandslos und bieten keine Setter- oder Getter-Funktionen. Tatsächlich werden Widgets nicht als Objekte offengelegt. Zum Aktualisieren der UI rufen Sie dieselbe zusammensetzbare Funktion mit verschiedenen Argumenten auf. Dies macht es einfach, einen Status für Architekturmuster wie eine ViewModel bereitzustellen, wie im Leitfaden zur Anwendungsarchitektur beschrieben. Anschließend sind Ihre zusammensetzbaren Funktionen dafür verantwortlich, bei jeder Aktualisierung der beobachtbaren Daten den aktuellen Anwendungsstatus in eine UI umzuwandeln.

Darstellung des Datenflusses in einer Compose-Benutzeroberfläche, von übergeordneten Objekten bis zu ihren untergeordneten Elementen.

Abbildung 2: Die App-Logik stellt der zusammensetzbaren Funktion der obersten Ebene Daten bereit. Diese Funktion verwendet die Daten, um die Benutzeroberfläche zu beschreiben. Dazu ruft sie andere zusammensetzbare Funktionen auf und übergibt die entsprechenden Daten an diese zusammensetzbaren Funktionen sowie an die nächste in der Hierarchie.

Wenn der Nutzer mit der UI interagiert, löst sie Ereignisse wie onClick aus. Diese Ereignisse sollten die Anwendungslogik benachrichtigen, die dann den Status der Anwendung ändern kann. Wenn sich der Status ändert, werden die zusammensetzbaren Funktionen mit den neuen Daten noch einmal aufgerufen. Dies führt dazu, dass die UI-Elemente neu gezeichnet werden. Dieser Vorgang wird als Neuzusammensetzung bezeichnet.

Darstellung, wie UI-Elemente auf Interaktionen reagieren, indem sie Ereignisse auslösen, die von der App-Logik verarbeitet werden

Abbildung 3: Der Nutzer hat mit einem UI-Element interagiert, wodurch ein Ereignis ausgelöst wurde. Die Anwendungslogik reagiert auf das Ereignis. Anschließend werden die zusammensetzbaren Funktionen bei Bedarf automatisch mit neuen Parametern aufgerufen.

Dynamischer Content

Da zusammensetzbare Funktionen in Kotlin und nicht in XML geschrieben werden, können sie genauso dynamisch sein wie jeder andere Kotlin-Code. Angenommen, Sie möchten eine UI erstellen, die eine Liste von Nutzern begrüßt:

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}

Diese Funktion verwendet eine Liste von Namen und generiert eine Begrüßung für jeden Nutzer. Zusammensetzbare Funktionen können sehr komplex sein. Mit if-Anweisungen können Sie entscheiden, ob ein bestimmtes UI-Element angezeigt werden soll. Sie können Schleifen verwenden. Sie können Hilfsfunktionen aufrufen. Sie haben die volle Flexibilität der zugrunde liegenden Sprache. Diese Leistungsfähigkeit und Flexibilität sind einer der wichtigsten Vorteile von Jetpack Composer.

Neuzusammensetzung

In einem imperativen UI-Modell rufen Sie zum Ändern eines Widgets einen Setter für das Widget auf, um seinen internen Status zu ändern. In Compose rufen Sie die zusammensetzbare Funktion noch einmal mit neuen Daten auf. Dies führt zu einer Neuzusammensetzung der Funktion – die von der Funktion ausgegebenen Widgets werden gegebenenfalls mit neuen Daten neu gezeichnet. Das Compose-Framework kann nur die geänderten Komponenten intelligent neu zusammensetzen.

Sehen Sie sich zum Beispiel die folgende zusammensetzbare Funktion an, die eine Schaltfläche anzeigt:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}

Bei jedem Klick auf die Schaltfläche aktualisiert der Anrufer den Wert von clicks. Compose ruft die Lambda-Funktion noch einmal mit der Text-Funktion auf, um den neuen Wert anzuzeigen. Dieser Vorgang wird als Neuzusammensetzung bezeichnet. Andere Funktionen, die nicht von diesem Wert abhängen, werden nicht neu zusammengesetzt.

Wie bereits erwähnt, kann die Neuzusammensetzung des gesamten UI-Baums rechenintensiv sein, was Rechenleistung und Akkulaufzeit erfordert. Compose löst dieses Problem mit dieser intelligenten Neuzusammensetzung.

Bei der Neuzusammensetzung werden die zusammensetzbaren Funktionen noch einmal aufgerufen, wenn sich Eingaben ändern. Dies geschieht, wenn sich die Eingaben der Funktion ändern. Wenn „Compose“ auf der Grundlage neuer Eingaben neu zusammengesetzt wird, werden nur die Funktionen oder Lambdas aufgerufen, die sich möglicherweise geändert haben, und der Rest wird übersprungen. Durch das Überspringen aller Funktionen oder Lambdas ohne geänderte Parameter kann Compose effizient neu zusammengesetzt werden.

Verlassen Sie sich niemals auf Nebeneffekte bei der Ausführung zusammensetzbarer Funktionen, da die Neuzusammensetzung einer Funktion übersprungen werden kann. Dies kann zu seltsamem und unvorhersehbarem Verhalten in Ihrer App führen. Ein Nebeneffekt ist jede Änderung, die für den Rest Ihrer App sichtbar ist. Diese Aktionen sind beispielsweise alle gefährliche Nebenwirkungen:

  • In eine Eigenschaft eines gemeinsam genutzten Objekts schreiben
  • Beobachtbares Objekt in ViewModel aktualisieren
  • Gemeinsame Einstellungen werden aktualisiert

Zusammensetzbare Funktionen können so oft wie jeden Frame neu ausgeführt werden, z. B. wenn eine Animation gerendert wird. Zusammensetzbare Funktionen sollten schnell sein, um Verzögerungen bei Animationen zu vermeiden. Wenn Sie aufwendige Vorgänge ausführen müssen, z. B. das Lesen von gemeinsamen Einstellungen, sollten Sie dies in einer Hintergrundkoroutine tun und das Ergebnis des Werts als Parameter an die zusammensetzbare Funktion übergeben.

Mit diesem Code wird beispielsweise eine zusammensetzbare Funktion erstellt, um einen Wert in SharedPreferences zu aktualisieren. Die zusammensetzbare Funktion sollte nicht anhand gemeinsamer Einstellungen selbst lesen oder schreiben. Stattdessen verschiebt dieser Code die Lese- und Schreibvorgänge in eine ViewModel in einer Hintergrundkoroutine. Die Anwendungslogik übergibt den aktuellen Wert mit einem Callback, um eine Aktualisierung auszulösen.

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

In diesem Dokument werden eine Reihe von Dingen erläutert, die Sie bei der Verwendung von „Compose“ beachten sollten:

  • Zusammensetzbare Funktionen können in beliebiger Reihenfolge ausgeführt werden.
  • Zusammensetzbare Funktionen können parallel ausgeführt werden.
  • Bei der Neuzusammensetzung werden so viele zusammensetzbare Funktionen und Lambdas wie möglich übersprungen.
  • Die Neuzusammensetzung ist optimistisch und kann abgebrochen werden.
  • Eine zusammensetzbare Funktion kann recht häufig ausgeführt werden, genauso oft wie jeder Frame einer Animation.

In den folgenden Abschnitten erfahren Sie, wie Sie zusammensetzbare Funktionen erstellen, die die Neuzusammensetzung unterstützen. Die Best Practice besteht in jedem Fall darin, die zusammensetzbaren Funktionen schnell, idempotent und frei von Nebeneffekten zu halten.

Zusammensetzbare Funktionen können in beliebiger Reihenfolge ausgeführt werden

Wenn Sie sich den Code für eine zusammensetzbare Funktion ansehen, gehen Sie möglicherweise davon aus, dass der Code in der Reihenfolge ausgeführt wird, in der er angezeigt wird. Das stimmt aber nicht unbedingt. Wenn eine zusammensetzbare Funktion Aufrufe anderer zusammensetzbarer Funktionen enthält, können diese Funktionen in beliebiger Reihenfolge ausgeführt werden. Bei der Funktion „Compose“ kann erkannt werden, dass einige UI-Elemente eine höhere Priorität als andere haben, und diese zuerst gezeichnet werden.

Angenommen, Sie haben folgenden Code, um drei Bildschirme in einem Tab-Layout zu zeichnen:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

Die Aufrufe von StartScreen, MiddleScreen und EndScreen können in beliebiger Reihenfolge erfolgen. Das bedeutet, dass Sie beispielsweise nicht StartScreen() eine globale Variable (einen Nebeneffekt) festlegen und MiddleScreen() diese Änderung nutzen können. Stattdessen muss jede dieser Funktionen für sich stehen.

Zusammensetzbare Funktionen können parallel ausgeführt werden

Mit Compose kann die Neuzusammensetzung optimiert werden, indem zusammensetzbare Funktionen parallel ausgeführt werden. Dadurch kann Compose mehrere Kerne nutzen und zusammensetzbare Funktionen, die nicht auf dem Bildschirm angezeigt werden, mit niedrigerer Priorität ausführen.

Diese Optimierung bedeutet, dass eine zusammensetzbare Funktion in einem Pool von Hintergrundthreads ausgeführt werden kann. Wenn eine zusammensetzbare Funktion eine Funktion in einem ViewModel aufruft, kann Compose diese Funktion gleichzeitig aus mehreren Threads aufrufen.

Damit sich Ihre Anwendung ordnungsgemäß verhält, sollten alle zusammensetzbaren Funktionen keine Nebeneffekte haben. Lösen Sie stattdessen Nebeneffekte von Callbacks wie onClick aus, die immer im UI-Thread ausgeführt werden.

Wenn eine zusammensetzbare Funktion aufgerufen wird, erfolgt der Aufruf möglicherweise in einem anderen Thread als der Aufrufer. Das bedeutet, dass Code, der Variablen in einer zusammensetzbaren Lambda-Funktion ändert, vermieden werden sollte – sowohl weil dieser Code nicht threadsicher ist, als auch weil er ein unzulässiger Nebeneffekt der zusammensetzbaren Lambda-Funktion ist.

Das folgende Beispiel zeigt eine zusammensetzbare Funktion, bei der eine Liste und ihre Anzahl angezeigt werden:

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

Dieser Code ist keine Nebeneffekte und wandelt die Eingabeliste in die UI um. Dieser Code eignet sich hervorragend zum Anzeigen einer kleinen Liste. Wenn die Funktion jedoch in eine lokale Variable schreibt, ist dieser Code nicht threadsicher oder korrekt:

@Composable
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
                items++ // Avoid! Side-effect of the column recomposing.
            }
        }
        Text("Count: $items")
    }
}

In diesem Beispiel wird items bei jeder Neuzusammensetzung geändert. Das kann jeder Frame einer Animation sein oder der Zeitpunkt, an dem die Liste aktualisiert wird. In beiden Fällen wird in der Benutzeroberfläche die falsche Anzahl angezeigt. Aus diesem Grund werden Schreibvorgänge wie diese in Composer nicht unterstützt. Wenn Sie diese Schreibvorgänge untersagen, kann das Framework Threads ändern, um zusammensetzbare Lambdas auszuführen.

Bei der Neuzusammensetzung werden möglichst viele übersprungen

Wenn Teile Ihrer UI ungültig sind, versucht die Compose-Funktion, nur die Teile neu zusammenzusetzen, die aktualisiert werden müssen. Das bedeutet, dass die zusammensetzbare Funktion einer einzelnen Schaltfläche übersprungen werden kann, ohne eine der über- oder darunterliegenden zusammensetzbaren Funktionen in der UI-Struktur auszuführen.

Jede zusammensetzbare Funktion und Lambda-Funktion kann sich von selbst wieder zusammensetzen. Das folgende Beispiel zeigt, wie bei der Neuzusammensetzung einige Elemente beim Rendern einer Liste übersprungen werden können:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.bodyLarge)
        Divider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

Unter Umständen wird bei einer Neuzusammensetzung jeder dieser Bereiche als einzige ausgeführt. Wenn sich header ändert, wird beim Erstellen möglicherweise zur Lambda Column gewechselt, ohne dass eines der übergeordneten Elemente ausgeführt wird. Beim Ausführen von Column kann die Funktion „Compose“ die Elemente von LazyColumn überspringen, wenn sich names nicht geändert hat.

Auch hier sollte die Ausführung aller zusammensetzbaren Funktionen oder Lambdas keine Nebenwirkungen haben. Wenn Sie einen Nebeneffekt ausführen müssen, lösen Sie ihn über einen Callback aus.

Die Neuzusammensetzung ist optimistisch

Die Neuzusammensetzung beginnt, wenn Compose davon ausgeht, dass sich die Parameter einer zusammensetzbaren Funktion geändert haben könnten. Die Neuzusammensetzung ist optimistisch. Das bedeutet, dass Compose die Neuzusammensetzung erwartet, bevor sich die Parameter wieder ändern. Wenn sich ein Parameter vor Abschluss der Neuzusammensetzung ändert, bricht Compose die Neuzusammensetzung möglicherweise ab und startet sie mit dem neuen Parameter neu.

Wird die Neuzusammensetzung abgebrochen, wird die UI-Struktur bei der Neuzusammensetzung durch „Compose“ verworfen. Wenn es Nebeneffekte gibt, die von der angezeigten UI abhängig sind, wird sie auch dann angewendet, wenn die Zusammensetzung abgebrochen wird. Dies kann zu einem inkonsistenten App-Status führen.

Alle zusammensetzbaren Funktionen und Lambdas müssen idempotent und frei von Nebeneffekten sein, damit eine optimistische Neuzusammensetzung möglich ist.

Zusammensetzbare Funktionen können recht häufig ausgeführt werden.

In einigen Fällen wird möglicherweise eine zusammensetzbare Funktion für jeden Frame einer UI-Animation ausgeführt. Wenn die Funktion kostspielige Vorgänge wie das Lesen aus dem Gerätespeicher ausführt, kann sie zu UI-Verzögerungen führen.

Wenn Ihr Widget beispielsweise versucht, Geräteeinstellungen zu lesen, könnte es diese Einstellungen möglicherweise hunderte Male pro Sekunde lesen, was katastrophale Auswirkungen auf die Leistung Ihrer App hat.

Wenn die zusammensetzbare Funktion Daten benötigt, sollten Parameter für die Daten definiert werden. Anschließend können Sie kostspielige Arbeit in einen anderen Thread außerhalb der Zusammensetzung verschieben und die Daten mit mutableStateOf oder LiveData an Compose übergeben.

Weitere Informationen

In den folgenden zusätzlichen Ressourcen finden Sie weitere Informationen zur Berücksichtigung von Schreibfunktionen und zusammensetzbaren Funktionen.

Videos