Denken in Textform

Jetpack Compose ist ein modernes deklaratives UI-Toolkit für Android. Mit der Funktion „Compose“ lässt sich die Anwendungs-UI einfacher schreiben und verwalten. Dazu wird eine deklarative API bereitgestellt, mit der Sie die App-UI rendern können, ohne die Front-End-Ansichten zu ändern. Diese Terminologie erfordert etwas Erklärung, aber die Implikationen sind wichtig für Ihr Anwendungsdesign.

Das deklarative Programmiermodell

In der Vergangenheit konnte eine Android-Ansichtshierarchie als Baum von UI-Widgets dargestellt werden. Wenn sich der Status der Anwendung aufgrund von Nutzerinteraktionen ändert, muss die UI-Hierarchie aktualisiert werden, um die aktuellen Daten anzuzeigen. Die gängigste Methode zum Aktualisieren der Benutzeroberfläche besteht darin, den Baum mit Funktionen wie findViewById() zu durchlaufen 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.

Durch manuelles Bearbeiten von Ansichten steigt die Wahrscheinlichkeit von Fehlern. Wenn ein Datenelement an mehreren Orten gerendert wird, kann man leicht vergessen, eine der Ansichten, in denen es angezeigt wird, zu aktualisieren. Es ist auch leicht, illegale Zustände zu schaffen, wenn zwei Updates auf unerwartete Weise in Konflikt stehen. Bei einer Aktualisierung kann beispielsweise der Wert eines Knotens festgelegt werden, der gerade aus der UI entfernt wurde. Im Allgemeinen wächst die Komplexität der Softwarewartung mit der Anzahl der Ansichten, die aktualisiert werden müssen.

In den letzten Jahren hat die gesamte Branche damit begonnen, auf ein deklaratives UI-Modell umzusteigen, das die Entwicklung und Aktualisierung von Benutzeroberflächen erheblich vereinfacht. Dabei wird konzeptionell der gesamte Bildschirm von Grund auf neu generiert und dann nur die erforderlichen Änderungen angewendet. Bei diesem Ansatz wird die komplexe manuelle Aktualisierung einer zustandsorientierten Ansichtshierarchie vermieden. Compose ist ein deklaratives UI-Framework.

Eine Herausforderung bei der Neugenerierung des gesamten Bildschirms besteht darin, dass sie im Hinblick auf Zeit, Rechenleistung und Akkunutzung potenziell kostspielig 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 Ihrer UI-Komponenten, wie unter Neuzusammensetzung beschrieben.

Eine einfache zusammensetzbare Funktion

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

Screenshot eines Smartphones, auf dem der Text zu sehen ist

Abbildung 1: Eine einfache zusammensetzbare Funktion, die Daten übergeben und damit ein Text-Widget auf dem Bildschirm rendert.

Einige beachtenswerte Informationen zu dieser Funktion:

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

  • Die Funktion nimmt Daten auf. Zusammensetzbare Funktionen können Parameter akzeptieren, mit denen die Anwendungslogik die UI beschreiben kann. In diesem Fall akzeptiert unser Widget ein String, sodass es den Nutzer mit seinem Namen begrüßen kann.

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

  • Die Funktion gibt nichts zurück. Erstellungsfunktionen, die UI-Elemente 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 Benutzeroberfläche ohne Nebenwirkungen wie das Ändern von Eigenschaften oder globalen Variablen.

    Im Allgemeinen sollten alle zusammensetzbaren Funktionen mit diesen Eigenschaften geschrieben werden. Die Gründe hierfür werden unter Neuzusammensetzung erläutert.

Der deklarative Paradigmenwechsel

Bei vielen imperativen objektorientierten UI-Toolkits initialisieren Sie die UI, indem Sie eine Baumstruktur von Widgets instanziieren. Dies geschieht häufig, indem Sie eine XML-Layoutdatei aufblasen. Jedes Widget behält seinen eigenen internen Status bei und stellt Getter- und Setter-Methoden bereit, die es der Anwendungslogik ermöglichen, mit dem Widget zu interagieren.

Beim deklarativen Ansatz von Compose sind Widgets relativ zustandslos und stellen keine Setter- oder Getter-Funktionen bereit. Tatsächlich werden Widgets nicht als Objekte bereitgestellt. Sie aktualisieren die Benutzeroberfläche, indem Sie dieselbe zusammensetzbare Funktion mit unterschiedlichen Argumenten aufrufen. Dies macht es einfach, Status für Architekturmuster wie ViewModel bereitzustellen, wie im Leitfaden zur Anwendungsarchitektur beschrieben. Die zusammensetzbaren Funktionen wandeln dann bei jeder Aktualisierung der beobachtbaren Daten den aktuellen Anwendungsstatus in eine UI um.

Abbildung des Datenflusses in einer Benutzeroberfläche zum Schreiben von Inhalten, von übergeordneten Objekten bis zu den untergeordneten Elementen.

Abbildung 2: Die Anwendungslogik stellt Daten für die zusammensetzbare Funktion der obersten Ebene bereit. Diese Funktion verwendet die Daten, um die UI zu beschreiben, indem andere zusammensetzbare Funktionen aufgerufen werden. Die entsprechenden Daten werden dann an diese zusammensetzbaren Funktionen und weiter nach unten in der Hierarchie übergeben.

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

Darstellung, wie UI-Elemente auf Interaktionen reagieren, indem Ereignisse ausgelöst werden, die von der App-Logik verarbeitet werden

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

Dynamischer Content

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

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

Diese Funktion nimmt aus einer Liste von Namen eine Begrüßung für jeden Nutzer auf. Zusammensetzbare Funktionen können ziemlich komplex sein. Mit if-Anweisungen können Sie festlegen, 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 Compose.

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. Rufen Sie in „Compose“ die zusammensetzbare Funktion mit neuen Daten noch einmal auf. Dies führt dazu, dass die Funktion neu zusammengesetzt wird. Die von der Funktion ausgegebenen Widgets werden gegebenenfalls mit neuen Daten neu gezeichnet. Das Framework „Compose“ kann nur die geänderten Komponenten intelligent neu zusammensetzen.

Betrachten Sie zum Beispiel diese zusammensetzbare Funktion, die eine Schaltfläche anzeigt:

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

Jedes Mal, wenn auf die Schaltfläche geklickt wird, aktualisiert der Aufrufer den Wert von clicks. Compose ruft die Lambda-Funktion mit der Funktion Text noch einmal auf, um den neuen Wert anzuzeigen. Dieser Vorgang wird als Neuzusammensetzung bezeichnet. Andere Funktionen, die nicht vom Wert abhängen, werden nicht neu zusammengesetzt.

Wie bereits erwähnt, kann die Neuerstellung des gesamten UI-Baums rechen teuer sein, was Rechenleistung und Akkulaufzeit beansprucht. Mit der Funktion „Compose“ lässt sich dieses Problem durch intelligente Neuzusammensetzung lösen.

Bei der Neuzusammensetzung werden die zusammensetzbaren Funktionen noch einmal aufgerufen, wenn sich Eingaben ändern. Dies geschieht, wenn sich die Eingaben der Funktion ändern. Wenn die Funktion „Compose“ auf Grundlage neuer Eingaben neu zusammensetzt, werden nur die Funktionen oder Lambdas aufgerufen, die sich möglicherweise geändert haben. Der Rest wird übersprungen. Wenn Sie alle Funktionen oder Lambdas ohne geänderte Parameter überspringen, können Sie in Compose effizient neu zusammensetzen.

Verlassen Sie sich nie auf Nebeneffekte, die sich aus der Ausführung zusammensetzbarer Funktionen ergeben, da die Neuzusammensetzung einer Funktion übersprungen werden kann. Andernfalls kann es zu merkwürdigem und unvorhersehbarem Verhalten Ihrer App kommen. Ein Nebeneffekt ist jede Änderung, die für den Rest der App sichtbar ist. Diese Aktionen sind beispielsweise alle gefährlichen Nebeneffekte:

  • In eine Property eines gemeinsam genutzten Objekts schreiben
  • Beobachtbares in ViewModel wird aktualisiert
  • Geteilte Einstellungen werden aktualisiert

Zusammensetzbare Funktionen können genauso oft wie jeder 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 teure Vorgänge ausführen müssen, z. B. das Lesen aus gemeinsamen Einstellungen, tun Sie dies in einer Hintergrundkoroutine und übergeben das Wertergebnis als Parameter an die zusammensetzbare Funktion.

Mit diesem Code wird beispielsweise eine zusammensetzbare Funktion erstellt, um einen Wert in SharedPreferences zu aktualisieren. Die zusammensetzbare Funktion sollte keine Daten aus gemeinsamen Präferenzen selbst lesen oder schreiben. Stattdessen verschiebt dieser Code die Lese- und Schreibvorgänge in ein 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 der Funktion „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, und zwar so oft wie jeder Frame einer Animation.

In den folgenden Abschnitten erfahren Sie, wie Sie zusammensetzbare Funktionen für die Neuzusammensetzung erstellen. In jedem Fall empfiehlt es sich, 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, nehmen Sie an, dass der Code in der angegebenen Reihenfolge ausgeführt 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“ wird erkannt, dass einige UI-Elemente eine höhere Priorität haben als andere, und sie zuerst zeichnen.

Angenommen, Sie verwenden Code wie diesen, 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 z. B. nicht von StartScreen() eine globale Variable (einen Nebeneffekt) festlegen und MiddleScreen() diese Änderung nutzen können. Stattdessen muss jede dieser Funktionen in sich geschlossen sein.

Zusammensetzbare Funktionen können parallel ausgeführt werden

Mit der Funktion „Compose“ lässt sich die Neuzusammensetzung optimieren, indem zusammensetzbare Funktionen parallel ausgeführt werden. Dadurch kann „Compose“ mehrere Kerne nutzen und zusammensetzbare Funktionen, die nicht auf dem Bildschirm angezeigt werden, mit einer niedrigeren 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 für einen ViewModel aufruft, ruft Composer diese Funktion möglicherweise gleichzeitig aus mehreren Threads auf.

Damit Ihre Anwendung korrekt funktioniert, sollten alle zusammensetzbaren Funktionen keine Nebeneffekte haben. Löse 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. Code, der Variablen in einem zusammensetzbaren Lambda ändert, sollte daher vermieden werden – sowohl weil dieser Code nicht Thread-sicher ist als auch, weil er eine unzulässige Nebenwirkung des zusammensetzbaren Lambda ist.

Das folgende Beispiel zeigt eine zusammensetzbare Funktion, in der eine Liste und die zugehörige 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 hat keine Nebeneffekte und wandelt die Eingabeliste in eine UI um. Dieser Code eignet sich gut zum Anzeigen einer kleinen Liste. Wenn die Funktion jedoch in eine lokale Variable schreibt, ist dieser Code nicht Thread-sicher 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 oder bei einer Aktualisierung der Liste sein. In beiden Fällen wird auf der Benutzeroberfläche die falsche Anzahl angezeigt. Aus diesem Grund werden solche Schreibvorgänge in Composer nicht unterstützt. Wenn diese Schreibvorgänge blockiert werden, kann das Framework Threads ändern, um zusammensetzbare Lambdas auszuführen.

Bei der Neuzusammensetzung wird so oft wie möglich übersprungen

Wenn Teile der UI ungültig sind, versucht Compose, nur die Teile neu zusammenzusetzen, die aktualisiert werden müssen. Das bedeutet, dass er möglicherweise überspringen kann, um die zusammensetzbare Funktion einer einzelnen Schaltfläche noch einmal auszuführen, ohne eine der darüber- oder darunter liegenden zusammensetzbaren Funktionen in der UI-Baumstruktur auszuführen.

Jede zusammensetzbare Funktion und jedes Lambda kann sich von selbst 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) }))
}

Jeder dieser Bereiche ist möglicherweise das einzige, was bei einer Neuzusammensetzung ausgeführt werden muss. Wenn sich header ändert, springt Compose möglicherweise zur Lambda-Funktion Column, ohne eines der übergeordneten Elemente auszuführen. Beim Ausführen von Column kann Compose die Elemente der LazyColumn überspringen, wenn sich names nicht geändert hat.

Auch hier sollten alle zusammensetzbaren Funktionen oder Lambdas frei von Nebeneffekten ausgeführt werden. Wenn Sie einen Nebeneffekt ausführen müssen, lösen Sie ihn über einen Callback aus.

Die Neuzusammensetzung ist optimistisch

Die Neuzusammensetzung beginnt immer dann, wenn von der Funktion „Compose“ angenommen wird, dass sich die Parameter einer zusammensetzbaren Funktion geändert haben. Die Neuzusammensetzung ist optimistisch. Das bedeutet, dass mit der Funktion „Compose“ davon ausgeht, die Neuzusammensetzung abzuschließen, bevor sich die Parameter wieder ändern. Wenn sich ein Parameter vor der Neuzusammensetzung ändert, wird die Neuzusammensetzung möglicherweise abgebrochen und mit dem neuen Parameter neu gestartet.

Wenn die Neuzusammensetzung abgebrochen wird, verwirft die Funktion die UI-Baumstruktur. Wenn es Nebeneffekte gibt, die von der angezeigten Benutzeroberfläche abhängen, wird der Nebeneffekt auch dann angewendet, wenn die Zusammensetzung abgebrochen wird. Dies kann zu einem inkonsistenten App-Status führen.

Achten Sie darauf, dass alle zusammensetzbaren Funktionen und Lambdas idempotent sind und keine Nebeneffekte auftreten, um eine optimistische Neuzusammensetzung zu verarbeiten.

Zusammensetzbare Funktionen werden möglicherweise recht häufig ausgeführt

In einigen Fällen kann eine zusammensetzbare Funktion für jeden Frame einer UI-Animation ausgeführt werden. Wenn die Funktion teure Vorgänge ausführt, z. B. das Lesen aus dem Gerätespeicher, kann die Funktion 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 Ihre zusammensetzbare Funktion Daten benötigt, sollte sie Parameter für die Daten definieren. Sie können dann teure Arbeit in einen anderen Thread außerhalb der Zusammensetzung verschieben und die Daten mit mutableStateOf oder LiveData an Compose übergeben.

Weitere Informationen

Weitere Informationen zu „Compose“- und zusammensetzbaren Funktionen finden Sie in den folgenden zusätzlichen Ressourcen.

Videos