Architekturebenen in Jetpack Compose

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

Jetpack Compose ist kein monolithisches Projekt, sondern besteht aus einer Reihe von Modulen, die zu einem vollständigen Stack zusammengesetzt werden. Wenn Sie die verschiedenen Module von Jetpack Compose kennen, haben Sie folgende Möglichkeiten:

  • Die richtige Abstraktionsebene für die Entwicklung Ihrer App oder Bibliothek verwenden
  • Informationen dazu, wann Sie zu einer niedrigeren Ebene wechseln können, um mehr Kontrolle oder Anpassungsmöglichkeiten zu erhalten
  • Abhängigkeiten minimieren

Ebenen

Die wichtigsten Schichten von Jetpack Compose sind:

Abbildung 1. Die Hauptschichten von Jetpack Compose.

Jede Schicht baut auf den darunter liegenden Schichten auf und kombiniert Funktionen, um Komponenten höherer Ebene zu erstellen. Jede Ebene baut auf öffentlichen APIs der darunter liegenden Ebenen auf, um die Modulgrenzen zu überprüfen und Sie bei Bedarf in die Lage zu versetzen, eine Ebene zu ersetzen. Sehen wir uns diese Schichten von unten nach oben an.

Laufzeit
In diesem Modul werden die Grundlagen der Compose-Laufzeit erläutert, z. B. remember, mutableStateOf, die Anmerkung @Composable und SideEffect. Sie können diese Ebene direkt verwenden, wenn Sie nur die Baumverwaltungsfunktionen von Compose und nicht die Benutzeroberfläche benötigen.
Benutzeroberfläche
Die UI-Ebene besteht aus mehreren Modulen ( z. B. ui-text, ui-graphics und ui-tooling). Diese Module implementieren die Grundlagen des UI-Toolkits, z. B. LayoutNode, Modifier, Eingabehandler, benutzerdefinierte Layouts und Zeichnen. Sie können diese Ebene verwenden, wenn Sie nur die grundlegenden Konzepte eines UI-Toolkits benötigen.
Grundlagen
Dieses Modul bietet designsystemunabhängige Bausteine für die Compose-Benutzeroberfläche, z. B. Row und Column, LazyColumn und die Erkennung bestimmter Touch-Gesten. Sie können die Basisschicht als Ausgangspunkt für Ihr eigenes Designsystem verwenden.
Material
Dieses Modul bietet eine Implementierung des Material Design-Systems für die Compose-Benutzeroberfläche, einschließlich eines Designsystems, stilisierter Komponenten, Ripple-Anzeige und Symbole. Wenn Sie Material Design in Ihrer App verwenden, bauen Sie auf dieser Ebene auf.

Designprinzipien

Ein Leitprinzip für Jetpack Compose besteht darin, kleine, fokussierte Funktionen bereitzustellen, die zusammengesetzt (oder komponiert) werden können, anstatt nur wenige monolithische Komponenten. Dieser Ansatz bietet eine Reihe von Vorteilen.

Umfassende Kontrolle

Komponenten auf höherer Ebene bieten in der Regel mehr Funktionen, beschränken aber Ihre direkte Kontrolle. Wenn Sie mehr Kontrolle benötigen, können Sie eine Komponente einer niedrigeren 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 API der unteren Ebene Animatable verwenden:

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

Die animateColorAsState API der höheren Ebene basiert selbst auf der Animatable API der niedrigeren Ebene. Die Verwendung der API der unteren 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 zusammenstellen, lassen sich die Komponenten bei Bedarf viel einfacher anpassen. Betrachten wir beispielsweise die Implementierung von Button, die von der Materialebene 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, das den Hintergrund, die Form, die Klickbehandlung usw. bereitstellt Surface

  2. Ein CompositionLocalProvider, mit dem sich das Alpha des Inhalts ändert, wenn die Schaltfläche aktiviert oder deaktiviert ist

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

  4. Eine Row gibt die Standardlayoutrichtlinie für den Inhalt der Schaltfläche an.

Wir haben einige Parameter und Kommentare weggelassen, um die Struktur klarer zu machen. Die gesamte Komponente besteht jedoch nur aus etwa 40 Codezeilen, da sie einfach diese vier Komponenten zur Implementierung der Schaltfläche zusammensetzt. Bei Komponenten wie Button ist es wichtig, welche Parameter freigegeben werden. Dabei wird ein Gleichgewicht zwischen gängigen Anpassungen und einer Explosion von Parametern geschaffen, die die Verwendung einer Komponente erschweren können. Material-Komponenten bieten beispielsweise Anpassungen, die im Material Design-System festgelegt sind, sodass die Material Design-Prinzipien leicht eingehalten werden können.

Wenn Sie jedoch eine Anpassung über die Parameter einer Komponente hinaus vornehmen möchten, können Sie eine Ebene nach unten wechseln und eine Komponente forken. Beispielsweise legt Material Design fest, dass Schaltflächen einen einfarbigen Hintergrund haben sollten. Wenn Sie einen Farbverlauf als Hintergrund benötigen, wird diese Option von den Button-Parametern nicht unterstützt. In diesem Fall können Sie die Material Button-Implementierung 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()
            }
        }
    }
}

In der obigen Implementierung werden weiterhin Komponenten aus der Material-Ebene verwendet, z. B. die Material-Konzepte aktueller Inhalts-Alphawert und der aktuelle Textstil. Es ersetzt jedoch das Material Surface durch ein Row und gestaltet es so, dass das gewünschte Erscheinungsbild erreicht wird.

Wenn Sie keine Material-Konzepte verwenden möchten, z. B. wenn Sie Ihr eigenes Designsystem erstellen, können Sie nur Komponenten der Basisschicht 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

Da Compose darauf ausgelegt ist, mehrschichtige, wiederverwendbare Komponenten zu erstellen, sollten Sie nicht immer die Bausteine der unteren Ebene verwenden. Viele Komponenten der höheren Ebene bieten nicht nur mehr Funktionen, sondern implementieren oft Best Practices wie die Unterstützung der Barrierefreiheit.

Wenn Sie Ihrer benutzerdefinierten Komponente beispielsweise Gestenunterstützung hinzufügen möchten, können Sie diese 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.

Als Faustregel gilt: Bauen Sie vorzugsweise auf der Komponente der höchsten Ebene auf, die die benötigte Funktionalität bietet, um von den Best Practices zu profitieren, die sie enthalten.

Weitere Informationen

Im Beispiel für Jetsnack finden Sie ein Beispiel für die Erstellung eines benutzerdefinierten Designsystems.