Architekturebenen in Jetpack Compose

Diese Seite bietet einen allgemeinen Überblick über die Architekturebenen, aus denen Jetpack Compose besteht, und die Grundprinzipien dieses Designs.

Jetpack Compose ist kein einzelnes monolithisches Projekt. Es wird aus einer Reihe von Modulen erstellt, die zu einem vollständigen Stack zusammengefügt sind. Wenn Sie die verschiedenen Module von Jetpack Compose kennen, können Sie:

  • Geeignete Abstraktionsebene zum Erstellen Ihrer App oder Bibliothek verwenden
  • Sie können ermitteln, wann Sie für mehr Kontrolle oder Anpassung auf eine niedrigere Ebene klicken können.
  • Abhängigkeiten minimieren

Ebenen

Die Hauptebenen von Jetpack Compose sind:

Abbildung 1: Die wichtigsten Ebenen von Jetpack Compose.

Jede Ebene baut auf den unteren Ebenen auf und kombiniert die 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, bei Bedarf jede Ebene zu ersetzen. Betrachten wir diese Schichten von unten nach oben.

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 zur Baumverwaltung von Composer und nicht für die Benutzeroberfläche benötigen.
Benutzeroberfläche
Die UI-Ebene besteht aus mehreren Modulen (ui-text, ui-graphics, ui-tooling usw.). Mit diesen Modulen werden die Grundlagen des UI-Toolkits implementiert, z. B. LayoutNode, Modifier, Eingabe-Handler, benutzerdefinierte Layouts und Zeichnungen. Sie können auf dieser Ebene aufbauen, wenn Sie nur grundlegende Konzepte eines UI-Toolkits benötigen.
Grundlagen
In diesem Modul finden Sie von Designsystem unabhängige Bausteine für die Compose-UI wie Row und Column, LazyColumn sowie die Erkennung bestimmter Gesten. Sie können auf der Basisebene aufbauen, um ein eigenes Designsystem zu erstellen.
Material
In diesem Modul wird eine Implementierung des Material Design-Systems für die Composer-UI bereitgestellt. Es enthält ein Designsystem, Komponenten mit benutzerdefiniertem Stil, Wellensymbole und Symbole. Bauen Sie auf dieser Ebene auf, wenn Sie Material Design in Ihrer App verwenden.

Designprinzipien

Ein Leitprinzip von Jetpack Compose besteht darin, kleine, gezielte Funktionalitätselemente bereitzustellen, die zusammen zusammengesetzt (oder zusammengefügt) werden können, anstatt nur ein paar monolithische Komponenten. Dieser Ansatz bietet eine Reihe von Vorteilen.

Steuerung

Komponenten auf höherer Ebene bieten Ihnen in der Regel mehr Vorteile, schränken jedoch Ihre direkte Kontrolle ein. Wenn Sie mehr Kontrolle benötigen, können Sie eine untergeordnete Komponente über das Drop-down-Menü 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 beginnen soll, ist dies mit dieser API nicht möglich. Stattdessen können Sie eine Drop-down-Liste erstellen, um die untergeordnete Animatable API zu verwenden:

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

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

Anpassbare

Wenn Sie übergeordnete Komponenten aus kleineren Bausteinen zusammenstellen, ist es deutlich einfacher, Komponenten bei Bedarf anzupassen. Ein Beispiel ist die Implementierung von Button, die über die 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 4 Komponenten:

  1. Ein Material Surface für den Hintergrund, die Form, das Verarbeiten von Klicks usw.

  2. Ein CompositionLocalProvider, das den Alphawert des Inhalts ändert, wenn die Schaltfläche aktiviert oder deaktiviert ist

  3. Mit ProvideTextStyle wird der Standardtextstil festgelegt, der verwendet werden soll.

  4. Ein Row stellt die Standard-Layoutrichtlinie für den Inhalt der Schaltfläche bereit

Wir haben einige Parameter und Kommentare weggelassen, um die Struktur zu verdeutlichen. Die gesamte Komponente besteht jedoch nur aus etwa 40 Codezeilen, da diese vier Komponenten einfach zusammengefügt werden, um die Schaltfläche zu implementieren. Komponenten wie Button bestimmen, welche Parameter sie verfügbar machen, und ermöglichen so gängige Anpassungen gegen eine explosionsartige Zunahme von Parametern, die die Nutzung einer Komponente erschweren. Materialkomponenten können beispielsweise im Material Design-System angepasst werden, was die Einhaltung der Prinzipien des Material-Designs erleichtert.

Wenn Sie jedoch eine Anpassung vornehmen möchten, die über die Parameter einer Komponente hinausgeht, können Sie eine Ebene per Dropdown-Menü aufklappen und eine Komponente verzweigen. Material Design gibt beispielsweise an, dass Schaltflächen einen einfarbigen Hintergrund haben sollten. Wenn Sie einen Farbverlaufshintergrund benötigen, wird diese Option von den Button-Parametern nicht unterstützt. In diesem Fall kannst du die Button-Implementierung von Material als Referenz verwenden und deine 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()
            }
        }
    }
}

Die obige Implementierung verwendet weiterhin Komponenten aus der Material-Ebene, z. B. die Material-Konzepte der aktuellen Inhalts-Alphaversion und des aktuellen Textstils. Allerdings wird das Material-Surface durch ein Row-Element ersetzt und es wird so gestaltet, dass es wie gewünscht aussieht.

Wenn Sie überhaupt keine Material-Konzepte verwenden möchten, z. B. wenn Sie Ihr eigenes maßgeschneidertes Designsystem erstellen, können Sie ausschließlich Grundlagenschichtkomponenten 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()
    }
}

Jetpack Compose reserviert die einfachsten Namen für die Komponenten der obersten Ebene. Beispielsweise basiert androidx.compose.material.Text auf androidx.compose.foundation.text.BasicText. So können Sie für Ihre eigene Implementierung den Namen mit der besten Auffindbarkeit festlegen, wenn Sie höhere Ebenen ersetzen möchten.

Die richtige Abstraktion auswählen

Die Philosophie von Compose besteht darin, mehrschichtige, wiederverwendbare Komponenten zu erstellen. Daher sollten Sie nicht immer nach den Bausteinen unterer Ebene greifen. Viele Komponenten einer höheren Ebene bieten nicht nur mehr Funktionen, sondern implementieren häufig auch Best Practices wie die Unterstützung von Bedienungshilfen.

Wenn Sie beispielsweise Ihrer benutzerdefinierten Komponente Unterstützung für Gesten hinzufügen möchten, können Sie diese von Grund auf mit Modifier.pointerInput erstellen. Darüber hinaus gibt es aber noch andere übergeordnete Komponenten, die einen besseren Ausgangspunkt bieten, z. B. Modifier.draggable, Modifier.scrollable oder Modifier.swipeable.

Sie sollten in der Regel lieber auf der Komponente der höchsten Ebene aufbauen, die die Funktionen bietet, die Sie benötigen, um von den darin enthaltenen Best Practices zu profitieren.

Weitere Informationen

Im Jetsnack-Beispiel finden Sie ein Beispiel für das Erstellen eines benutzerdefinierten Designsystems.