Denken in Textform

Jetpack Compose ist ein modernes deklaratives UI-Toolkit für Android. Compose vereinfacht das Schreiben und Verwalten der UI Ihrer App, indem es eine deklarative API bietet, mit der Sie die UI Ihrer App rendern können, ohne Frontend-Ansichten imperativ zu ändern. Diese Terminologie erfordert eine Erklärung, aber die Auswirkungen sind wichtig für das Design Ihrer App.

Das deklarative Programmierparadigma

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

Das manuelle Bearbeiten von Ansichten erhöht die Wahrscheinlichkeit von Fehlern. Wenn ein Datenelement an mehreren Stellen gerendert wird, vergessen Sie möglicherweise, eine der Ansichten zu aktualisieren, in der es angezeigt wird. Dies kann auch zu ungültigen Zuständen führen, wenn zwei Updates auf unerwartete Weise in Konflikt geraten. Beispielsweise kann ein Update versuchen, einen Wert für einen Knoten festzulegen, der gerade aus der UI entfernt wurde. Im Allgemeinen steigt die Komplexität der Softwarewartung mit der Anzahl der Ansichten, die aktualisiert werden müssen.

In den letzten Jahren hat sich die gesamte Branche auf ein deklaratives UI-Modell umgestellt, das das Erstellen und Aktualisieren von Benutzeroberflächen vereinfacht.

Bei dieser Technik wird der gesamte Bildschirm konzeptionell von Grund auf neu generiert und dann werden nur die erforderlichen Änderungen angewendet. Mit diesem Ansatz wird die Komplexität der manuellen Aktualisierung einer zustandsbehafteten Ansichtshierarchie vermieden. Compose ist ein deklaratives UI-Framework.

Eine Herausforderung beim Regenerieren des gesamten Bildschirms besteht darin, dass es potenziell teuer ist, was Zeit, Rechenleistung und Akkunutzung angeht. Um diese Kosten zu senken, wählt Compose intelligent aus, welche Teile der UI zu einem bestimmten Zeitpunkt neu gezeichnet werden müssen. Dies hat einige Auswirkungen auf das Design Ihrer UI-Komponenten, wie unter Neukomposition beschrieben.

Beispiel für eine zusammensetzbare Funktion

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

Ein Smartphone, auf dem der Text „Hello World“ angezeigt wird, und der Code für die zusammensetzbare Funktion, die diese Benutzeroberfläche generiert.
Abbildung 1 Eine zusammensetzbare Funktion, an die Daten übergeben werden und die damit ein Text-Widget auf dem Bildschirm rendert.

Einige bemerkenswerte Dinge zu dieser Funktion:

  • Annotation:Die Funktion ist mit der Annotation @Composable versehen. Alle zusammensetzbaren Funktionen müssen diese Annotation haben. Diese Annotation informiert den Compose-Compiler darüber, dass diese Funktion Daten in UI umwandeln soll.
  • Dateneingabe:Die Funktion nimmt Daten auf. Zusammensetzbare Funktionen können Parameter akzeptieren, mit denen die App-Logik die UI beschreiben kann. In diesem Fall akzeptiert unser Widget einen String, damit der Nutzer mit Namen begrüßt werden kann.
  • UI-Anzeige:Die Funktion zeigt Text in der UI 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.
  • Kein Rückgabewert:Die Funktion gibt nichts zurück. Compose-Funktionen, die UI ausgeben, müssen nichts zurückgeben, da sie den Zielbildschirmstatus beschreiben, anstatt UI-Widgets zu erstellen.
  • Eigenschaften: Diese Funktion ist schnell, idempotent und nebenwirkungsfrei.

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

    Im Allgemeinen müssen alle zusammensetzbaren Funktionen mit diesen Eigenschaften geschrieben werden. Die Gründe dafür werden unter Neukomposition erläutert.

Der Paradigmenwechsel zu deklarativ

Bei vielen imperativen objektorientierten UI-Toolkits initialisieren Sie die UI, indem Sie einen Baum von Widgets instanziieren. Das geschieht oft durch das Aufblasen einer XML-Layoutdatei. Jedes Widget verwaltet seinen eigenen internen Status und stellt Getter- und Setter-Methoden bereit, mit denen die App-Logik mit dem Widget interagieren kann.

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 UI, indem Sie dieselbe zusammensetzbare Funktion mit anderen Argumenten aufrufen. Dadurch wird die Bereitstellung des Status für Architekturmuster wie ein ViewModel vereinfacht, wie im Leitfaden zur App-Architektur beschrieben. Dann sind Ihre zusammensetzbaren Funktionen dafür verantwortlich, den aktuellen Anwendungsstatus jedes Mal in eine UI umzuwandeln, wenn die beobachtbaren Daten aktualisiert werden.

Abbildung des Datenflusses in einer Compose-Benutzeroberfläche, von Objekten auf hoher Ebene bis hin zu ihren untergeordneten Elementen.
Abbildung 2. Die App-Logik stellt Daten für die zusammensetzbare Funktion der obersten Ebene bereit. Diese Funktion verwendet die Daten, um die UI zu beschreiben, indem sie andere zusammensetzbare Funktionen aufruft, und übergibt die entsprechenden Daten an diese zusammensetzbaren Funktionen und weiter nach unten in der Hierarchie.

Wenn der Nutzer mit der UI interagiert, löst die UI Ereignisse wie onClick aus. Diese Ereignisse sollten die App-Logik 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. Dadurch werden die UI-Elemente neu gezeichnet. Dieser Vorgang wird als Neukomposition bezeichnet.

Abbildung, die zeigt, 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 App-Logik reagiert auf das Ereignis. Dann werden die zusammensetzbaren Funktionen bei Bedarf automatisch mit neuen Parametern noch einmal aufgerufen.

Dynamischer Inhalt

Da zusammensetzbare Funktionen in Kotlin anstelle von XML geschrieben werden, können sie so 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 nimmt eine Liste von Namen auf 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 ist einer der Hauptvorteile von Jetpack Compose.

Neukomposition

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 mit neuen Daten noch einmal auf. Dadurch wird die Funktion neu zusammengesetzt. Die von der Funktion ausgegebenen Widgets werden bei Bedarf mit neuen Daten neu gezeichnet. Das Compose-Framework kann intelligent nur die Komponenten neu zusammensetzen, die sich geändert haben.

Betrachten Sie beispielsweise 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 Neukomposition bezeichnet. Andere Funktionen, die nicht vom Wert abhängen, werden nicht neu zusammengesetzt.

Wie bereits erwähnt, kann das Neuzusammensetzen des gesamten UI-Baums rechenintensiv sein, was Rechenleistung und Akkulaufzeit beansprucht. Compose löst dieses Problem mit der intelligenten Neukomposition.

Die Neukomposition ist der Vorgang, bei dem Ihre zusammensetzbaren Funktionen noch einmal aufgerufen werden, wenn sich die Eingaben ändern. Wenn Compose basierend auf neuen Eingaben neu zusammensetzt, werden nur die Funktionen oder Lambda-Funktionen aufgerufen, die sich möglicherweise geändert haben. Die übrigen werden übersprungen. Durch das Überspringen von Funktionen oder Lambda-Funktionen mit unveränderten Parametern kann Compose effizient neu zusammensetzen.

Verlassen Sie sich niemals auf Nebenwirkungen bei der Ausführung zusammensetzbarer Funktionen, da die Neukomposition einer Funktion übersprungen werden kann. Andernfalls können Nutzer seltsames und unvorhersehbares Verhalten in Ihrer App feststellen. Eine Nebenwirkung ist jede Änderung, die für den Rest Ihrer App sichtbar ist. Die folgenden Aktionen sind beispielsweise gefährliche Nebenwirkungen:

  • In eine Eigenschaft eines freigegebenen Objekts schreiben
  • Ein beobachtbares Element in ViewModel aktualisieren
  • Freigegebene Einstellungen aktualisieren

Zusammensetzbare Funktionen können so oft wie in jedem Frame neu ausgeführt werden, z. B. wenn eine Animation gerendert wird. Zusammensetzbare Funktionen sollten schnell sein, um Ruckeln bei Animationen zu vermeiden. Wenn Sie aufwendige Vorgänge ausführen müssen, z. B. das Lesen aus freigegebenen Einstellungen, führen Sie sie in einer Hintergrund-Coroutine aus und übergeben Sie das Ergebnis als Parameter an die zusammensetzbare Funktion.

In diesem Code wird beispielsweise eine zusammensetzbare Funktion erstellt, um einen Wert in SharedPreferences zu aktualisieren. Die zusammensetzbare Funktion sollte nicht selbst aus freigegebenen Einstellungen lesen oder in sie schreiben. Stattdessen werden in diesem Code die Lese- und Schreibvorgänge in eine ViewModel in einer Hintergrund-Coroutine verschoben. Die App-Logik ü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 einige Dinge behandelt, die Sie bei der Verwendung von Compose beachten sollten:

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

In den folgenden Abschnitten wird beschrieben, wie Sie zusammensetzbare Funktionen erstellen, um die Neukomposition zu unterstützen. In jedem Fall ist es am besten, wenn Ihre zusammensetzbaren Funktionen schnell, idempotent und nebenwirkungsfrei sind.

Bei der Neukomposition wird so viel wie möglich übersprungen

Wenn Teile Ihrer UI ungültig sind, versucht Compose, nur die Teile neu zusammenzusetzen, die aktualisiert werden müssen. Das bedeutet, dass die Neuausführung der zusammensetzbaren Funktion einer einzelnen Button-Funktion übersprungen werden kann, ohne dass die zusammensetzbaren Funktionen höher oder niedriger im UI-Baum ausgeführt werden.

Jede zusammensetzbare Funktion und Lambda-Funktion kann selbst neu zusammengesetzt werden. Das folgende Beispiel zeigt, wie bei der Neukomposition 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)
        HorizontalDivider()

        // 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 kann das Einzige sein, das während einer Neukomposition ausgeführt wird. Compose kann die Neuausführung der Lambda-Funktion Column überspringen, ohne dass die übergeordneten Elemente ausgeführt werden, wenn sich der header ändert. Und bei der Ausführung von Column kann Compose die Elemente von überspringen, wenn sich LazyColumn nicht geändert hat.names

Auch hier gilt, dass alle zusammensetzbaren Funktionen oder Lambda-Funktionen nebenwirkungsfrei sein sollten. Wenn Sie eine Nebenwirkung auslösen müssen, tun Sie dies über einen Callback.

Die Neukomposition ist optimistisch

Die Neukomposition beginnt, wenn Compose der Meinung ist, dass sich die Parameter einer zusammensetzbaren Funktion geändert haben könnten. Die Neukomposition ist optimistisch. Das bedeutet, dass Compose davon ausgeht, die Neukomposition abzuschließen, bevor sich die Parameter wieder ändern. Wenn sich ein Parameter tatsächlich ändert, bevor die Neukomposition abgeschlossen ist, kann Compose die Neukomposition abbrechen und mit dem neuen Parameter neu starten.

Wenn die Neukomposition abgebrochen wird, verwirft Compose den UI-Baum aus der Neukomposition. Wenn Sie Nebenwirkungen haben, die davon abhängen, dass die UI angezeigt wird, wird die Nebenwirkung auch dann angewendet, wenn die Komposition abgebrochen wird. Dies kann zu einem inkonsistenten App-Status führen.

Prüfen Sie, ob alle zusammensetzbaren Funktionen und Lambda-Funktionen idempotent und nebenwirkungsfrei sind, um die optimistische Neukomposition zu verarbeiten.

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

In einigen Fällen kann eine zusammensetzbare Funktion für jeden Frame einer UI-Animation ausgeführt werden. Wenn die Funktion aufwendige Vorgänge ausführt, z. B. das Lesen aus dem Gerätespeicher, kann sie zu Ruckeln in der UI führen.

Wenn Ihr Widget beispielsweise versucht, Geräteeinstellungen zu lesen, könnte es diese Einstellungen potenziell Hunderte Male pro Sekunde lesen, was sich verheerend auf die Leistung Ihrer App auswirken würde.

Wenn eine zusammensetzbare Funktion Daten benötigt, definieren Sie Parameter für diese Daten. Sie können dann aufwendige Vorgänge in einen anderen Thread außerhalb der Komposition verschieben und den resultierenden Wert mit mutableStateOf oder LiveData als Parameter an die zusammensetzbare Funktion übergeben.

Zusammensetzbare Funktionen können parallel ausgeführt werden

Compose könnte die Neukomposition optimieren, indem zusammensetzbare Funktionen parallel ausgeführt werden. Dadurch könnte Compose mehrere Kerne nutzen und zusammensetzbare Funktionen, die nicht auf dem Bildschirm angezeigt werden, mit niedrigerer Priorität ausführen.

Diese Optimierung würde bedeuten, dass eine zusammensetzbare Funktion in einem Pool von Hintergrundthreads ausgeführt werden könnte. Wenn eine zusammensetzbare Funktion eine Funktion für ein ViewModel aufruft, kann Compose diese Funktion gleichzeitig von mehreren Threads aufrufen.

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

Wenn eine zusammensetzbare Funktion aufgerufen wird, kann der Aufruf in einem anderen Thread als der Aufrufer erfolgen. Daher sollte Code vermieden werden, der Variablen in einer zusammensetzbaren Lambda-Funktion ändert. Das liegt daran, dass solcher Code nicht threadsicher ist und eine unzulässige Nebenwirkung der zusammensetzbaren Lambda-Funktion darstellt.

Hier ein Beispiel für eine zusammensetzbare Funktion, die eine Liste und ihre Anzahl anzeigt:

@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 nebenwirkungsfrei und wandelt die Eingabeliste in 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
fun ListWithBug(myList: List<String>) {
    var items = 0

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

In diesem Beispiel wird items bei jeder Neukomposition geändert. Das kann in jedem Frame einer Animation oder bei der Aktualisierung der Liste der Fall sein. In beiden Fällen wird in der UI die falsche Anzahl angezeigt. Aus diesem Grund werden solche Schreibvorgänge in Compose nicht unterstützt. Durch das Verhindern dieser Schreibvorgänge kann das Framework Threads ändern, um zusammensetzbare Lambda-Funktionen auszuführen.

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 ist aber nicht unbedingt der Fall. Wenn eine zusammensetzbare Funktion Aufrufe an andere zusammensetzbare Funktionen enthält, können diese Funktionen in beliebiger Reihenfolge ausgeführt werden. Compose kann erkennen, dass einige UI-Elemente eine höhere Priorität haben als andere, und sie zuerst zeichnen.

Angenommen, Sie haben 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 beispielsweise nicht mit StartScreen() eine globale Variable festlegen (eine Nebenwirkung) und mit MiddleScreen() diese Änderung nutzen können. Stattdessen muss jede dieser Funktionen in sich abgeschlossen sein.

Weitere Informationen

Weitere Informationen zur Denkweise in Compose und zu zusammensetzbaren Funktionen finden Sie in den folgenden zusätzlichen Ressourcen.

Videos