Architekturebenen in Jetpack Compose

Auf dieser Seite erhalten Sie einen allgemeinen Überblick über die Architekturschichten von Jetpack Compose und die wichtigsten Prinzipien, die diesem Design zugrunde liegen.

Jetpack Compose ist kein einzelnes monolithisches Projekt, sondern besteht aus einer Reihe von Modulen, die zusammen einen vollständigen Stack bilden. Wenn Sie die verschiedenen Module von Jetpack Compose kennen, können Sie:

  • Geeignete Abstraktionsebene für die Entwicklung Ihrer App oder Bibliothek verwenden
  • Wissen, wann Sie für mehr Kontrolle oder Anpassungsmöglichkeiten auf eine niedrigere Ebene wechseln können
  • Abhängigkeiten minimieren

Ebenen

Die wichtigsten Ebenen von Jetpack Compose sind:

Abbildung 1: Die wichtigsten Ebenen von Jetpack Compose.

Jede Ebene baut auf den unteren Ebenen auf und kombiniert Funktionen, um Komponenten auf höherer Ebene zu erstellen. Jede Ebene baut auf öffentlichen APIs der unteren Ebenen auf, um die Modulgrenzen zu überprüfen und es Ihnen zu ermöglichen, jede Ebene bei Bedarf zu ersetzen. Sehen wir uns diese Ebenen von unten nach oben an.

Laufzeit
Dieses Modul bietet die Grundlagen der Compose-Laufzeit, z. B. remember, mutableStateOf, die Annotation @Composable und SideEffect. Sie können direkt auf dieser Ebene aufbauen, wenn Sie nur die Funktionen von Compose zur Baumverwaltung und nicht die Benutzeroberfläche benötigen.
Benutzeroberfläche
Die UI-Ebene besteht aus mehreren Modulen (ui-text, ui-graphics, ui-tooling usw.). Diese Module implementieren die Grundlagen des UI-Toolkits, z. B. LayoutNode, Modifier, Eingabehandler, benutzerdefinierte Layouts und das Zeichnen. Sie sollten diese Ebene in Betracht ziehen, wenn Sie nur grundlegende Konzepte eines UI-Toolkits benötigen.
Grundlage
Dieses Modul bietet designsystemunabhängige Bausteine für Compose UI, z. B. Row und Column, LazyColumn, Erkennung bestimmter Gesten usw. Sie können auf der Fundierungsebene aufbauen, um Ihr eigenes Designsystem zu erstellen.
Material
Dieses Modul bietet eine Implementierung des Material Design-Systems für Compose UI mit einem Theming-System, formatierten Komponenten, Ripple-Indikatoren und Symbolen. Diese Ebene ist die Grundlage für die Verwendung von Material Design in Ihrer App.

Designprinzipien

Ein Leitprinzip für Jetpack Compose ist es, kleine, fokussierte Funktionseinheiten bereitzustellen, die zusammengesetzt (oder komponiert) werden können, anstatt weniger monolithischer Komponenten. Dieser Ansatz bietet eine Reihe von Vorteilen.

Umfassende Kontrolle

Komponenten auf höherer Ebene bieten in der Regel mehr Funktionen, schränken aber die direkte Kontrolle ein. Wenn Sie mehr Kontrolle benötigen, können Sie eine Komponente auf niedrigerer Ebene verwenden.

Wenn Sie beispielsweise die Farbe einer Komponente animieren möchten, können Sie die animateColorAsState API verwenden:

val color = animateColorAsState(if (condition) Color.Green else Color.Red)

Wenn die Komponente jedoch immer grau sein soll, ist das mit dieser API nicht möglich. Stattdessen können Sie die Animatable API auf niedrigerer Ebene verwenden:

val color = remember { Animatable(Color.Gray) }
LaunchedEffect(condition) {
    color.animateTo(if (condition) Color.Green else Color.Red)
}

Die animateColorAsState-API auf höherer Ebene basiert auf der Animatable-API auf niedrigerer Ebene. Die Verwendung der API auf niedrigerer Ebene ist komplexer, bietet aber mehr Kontrolle. Wählen Sie die Abstraktionsebene aus, die Ihren Anforderungen am besten entspricht.

Personalisierung

Wenn Sie Komponenten höherer Ebene aus kleineren Bausteinen zusammensetzen, ist es viel einfacher, sie bei Bedarf anzupassen. Sehen Sie sich beispielsweise die Implementierung von Button an, die von der Material-Ebene bereitgestellt wird:

@Composable
fun Button(
    // …
    content: @Composable RowScope.() -> Unit
) {
    Surface(/* … */) {
        CompositionLocalProvider(/* … */) { // set LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                Row(
                    // …
                    content = content
                )
            }
        }
    }
}

Ein Button besteht aus vier Komponenten:

  1. Ein Material Surface für den Hintergrund, die Form, die Klickbehandlung usw.

  2. A CompositionLocalProvider damit sich der Alphawert des Inhalts ändert, wenn die Schaltfläche aktiviert oder deaktiviert wird

  3. Mit A ProvideTextStyle wird der zu verwendende Standardtextstil festgelegt.

  4. Eine Row stellt die Standardrichtlinie für das Layout des Inhalts der Schaltfläche bereit.

Wir haben einige Parameter und Kommentare weggelassen, um die Struktur zu verdeutlichen. Die gesamte Komponente umfasst jedoch nur etwa 40 Codezeilen, da sie lediglich diese vier Komponenten zusammenfügt, um die Schaltfläche zu implementieren. Bei Komponenten wie Button wird festgelegt, welche Parameter verfügbar sind. Dabei wird ein Gleichgewicht zwischen der Möglichkeit, häufige Anpassungen vorzunehmen, und einer Vielzahl von Parametern geschaffen, die die Verwendung einer Komponente erschweren können. Material-Komponenten bieten beispielsweise Anpassungen, die im Material Design-System angegeben sind. So lassen sich die Material Design-Grundsätze leicht einhalten.

Wenn Sie jedoch eine Anpassung vornehmen möchten, die über die Parameter einer Komponente hinausgeht, können Sie eine Ebene tiefer gehen und eine Komponente forken. In Material Design wird beispielsweise festgelegt, dass Schaltflächen einen einfarbigen Hintergrund haben sollten. Wenn Sie einen Hintergrund mit Farbverlauf benötigen, wird diese Option von den Button-Parametern nicht unterstützt. In diesem Fall können Sie die Material-Implementierung Button als Referenz verwenden und Ihre eigene Komponente erstellen:

@Composable
fun GradientButton(
    // …
    background: List<Color>,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(
                Brush.horizontalGradient(background)
            )
    ) {
        CompositionLocalProvider(/* … */) { // set material LocalContentAlpha
            ProvideTextStyle(MaterialTheme.typography.button) {
                content()
            }
        }
    }
}

Bei der oben genannten Implementierung werden weiterhin Komponenten aus der Material-Ebene verwendet, z. B. die Material-Konzepte für current content alpha (Alpha des aktuellen Inhalts) und den aktuellen Textstil. Dabei wird das Material Surface durch ein Row ersetzt und so formatiert, dass das gewünschte Aussehen erzielt wird.

Wenn Sie Material-Konzepte überhaupt nicht verwenden möchten, z. B. wenn Sie Ihr eigenes benutzerdefiniertes Designsystem erstellen, können Sie nur Komponenten der Fundierungsebene verwenden:

@Composable
fun BespokeButton(
    // …
    backgroundColor: Color,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Row(
        // …
        modifier = modifier
            .clickable(onClick = {})
            .background(backgroundColor)
    ) {
        // No Material components used
        content()
    }
}

In Jetpack Compose sind die einfachsten Namen für die Komponenten der obersten Ebene reserviert. Beispiel: androidx.compose.material.Text basiert auf androidx.compose.foundation.text.BasicText. So können Sie Ihrer eigenen Implementierung den am besten auffindbaren Namen geben, wenn Sie höhere Ebenen ersetzen möchten.

Die richtige Abstraktion auswählen

Die Philosophie von Compose, geschichtete, wiederverwendbare Komponenten zu erstellen, bedeutet, dass Sie nicht immer auf die untergeordneten Bausteine zurückgreifen sollten. Viele Komponenten der höheren Ebene bieten nicht nur mehr Funktionen, sondern implementieren oft auch Best Practices wie die Unterstützung der Barrierefreiheit.

Wenn Sie beispielsweise Ihrer benutzerdefinierten Komponente Unterstützung für Gesten hinzufügen möchten, können Sie diese Funktion mit Modifier.pointerInput von Grund auf neu erstellen. Es gibt jedoch andere Komponenten auf höherer Ebene, die darauf aufbauen und einen besseren Ausgangspunkt bieten, z. B. Modifier.draggable, Modifier.scrollable oder Modifier.swipeable.

In der Regel sollten Sie die Komponente der höchsten Ebene verwenden, die die benötigte Funktionalität bietet, um von den darin enthaltenen Best Practices zu profitieren.

Weitere Informationen

Ein Beispiel für das Erstellen eines benutzerdefinierten Designsystems finden Sie im Jetsnack-Beispiel.