Architekturebenen in Jetpack Compose

Diese Seite bietet einen allgemeinen Überblick über die Architekturebenen, aus denen Jetpack Compose besteht, und die Grundprinzipien, die diesem Design zugrunde liegen.

Jetpack Compose ist kein einzelnes monolithisches Projekt. Es besteht aus mehreren Modulen, die zu einem vollständigen Stack zusammengefügt 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. Untersuchen wir diese Ebenen von unten nach oben.

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, nicht aber 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, anstelle einiger monolithischer Komponenten kleine, fokussierte Funktionen bereitzustellen, die zusammen zusammengesetzt (oder zusammengesetzt) werden können. Dieser Ansatz bietet eine Reihe von Vorteilen.

Umfassende Kontrolle

Komponenten 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 beginnen soll, ist dies 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 übergeordnete animateColorAsState API baut wiederum auf der niedrigeren Animatable API auf. 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 Sie zum Beispiel die Implementierung von Button, die durch die 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, das den Hintergrund, die Form, die Klickbehandlung usw. bereitstellt Surface

  2. Ein CompositionLocalProvider, das die Alphawerte 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 gestalten. Die gesamte Komponente umfasst jedoch nur 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 im Material Design-System spezifizierte Anpassungen, was die Einhaltung der Material-Design-Prinzipien erleichtert.

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 kannst du die Material-Implementierung Button 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()
            }
        }
    }
}

Für die obige Implementierung werden weiterhin Komponenten aus der Material-Ebene verwendet, z. B. die Konzepte von Material für das aktuelle Inhalts-Alpha und den aktuellen 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. Beispielsweise basiert androidx.compose.material.Text auf androidx.compose.foundation.text.BasicText. Wenn Sie höhere Ebenen ersetzen möchten, können Sie auf diese Weise Ihre eigene Implementierung mit dem am besten auffindbaren Namen verwenden.

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 enthaltenen Best Practices zu profitieren.

Weitere Informationen

Im Beispiel Jetsnack wird gezeigt, wie ein benutzerdefiniertes Designsystem erstellt wird.