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 verstehen, können Sie:

  • Geeignete Abstraktionsebene verwenden, um Ihre App oder Bibliothek zu erstellen
  • Finden Sie heraus, wann Sie mit einem Drop-down-Menü auf eine niedrigere Ebene wechseln können, um mehr Kontrolle oder Anpassung zu haben.
  • 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, wodurch verschiedene Funktionen kombiniert werden, um übergeordnete Komponenten zu erstellen. Jede Ebene baut auf öffentlichen APIs der unteren Ebenen auf, um die Modulgrenzen zu überprüfen und es Ihnen bei Bedarf zu ermöglichen, jede Ebene zu ersetzen. Untersuchen wir diese Ebenen von unten nach oben.

Laufzeit
In diesem Modul werden die Grundlagen der Compose-Laufzeit beschrieben, z. B. remember, mutableStateOf, die Annotation @Composable und SideEffect. Sie können direkt auf dieser Ebene aufbauen, wenn Sie nur die Funktionen zur Baumverwaltung und nicht die UI von Composer benötigen.
Benutzeroberfläche
Die UI-Ebene besteht aus mehreren Modulen (ui-text, ui-graphics, ui-tooling usw.). In 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 die grundlegenden Konzepte eines UI-Toolkits benötigen.
Grundlage
Dieses Modul enthält Designsystem-unabhängige Bausteine für die Compose-UI, z. B. Row und Column, LazyColumn, die Erkennung bestimmter Gesten usw. Sie können auf der Fundamentebene aufbauen, um Ihr eigenes Designsystem zu erstellen.
Material
In diesem Modul wird eine Implementierung des Material Design-Systems für die Erstellungs-UI beschrieben. Dabei werden ein Designsystem, Komponenten mit benutzerdefinierten Stilen, Wellenangaben und Symbole bereitgestellt. Baue auf dieser Ebene auf, wenn du Material Design in deiner App verwendest.

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.

Steuerung

Komponenten auf höherer Ebene leisten in der Regel mehr für Sie, schränken jedoch die direkte Kontrolle ein, die Sie haben. Wenn Sie mehr Kontrolle benötigen, können Sie das Drop-down-Menü verwenden, um eine Komponente auf einer niedrigeren Ebene zu 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 im Drop-down-Menü die untergeordnete API 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 untergeordneten API ist komplexer, bietet aber mehr Kontrolle. Wählen Sie die Abstraktionsebene, die Ihren Anforderungen am besten entspricht.

Personalisierung

Das Zusammensetzen übergeordneter Komponenten aus kleineren Bausteinen macht es bei Bedarf wesentlich einfacher, Komponenten anzupassen. Hier ein Beispiel für 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 Surface, das Hintergrund, Form, Klickbehandlung usw. bereitstellt.

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

  3. Mit ProvideTextStyle wird der zu verwendende Standardtextstil festgelegt.

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

Wir haben einige Parameter und Kommentare weggelassen, um die Struktur klarer zu gestalten. 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 wird berücksichtigt, welche Parameter verfügbar gemacht werden. So lassen sich allgemeine Anpassungen vermeiden, da die Parameter oft verwendet werden, die eine Komponente erschweren. Material-Komponenten bieten beispielsweise im Material-Design-System spezifizierte Anpassungen, die die Einhaltung der Material-Design-Prinzipien erleichtern.

Wenn Sie jedoch eine Anpassung vornehmen möchten, die über die Parameter einer Komponente hinausgeht, können Sie eine Ebene auf „Dropdown“ setzen und eine Komponente abspalten. Zum Beispiel gibt Material Design an, dass Schaltflächen einen einfarbigen Hintergrund haben sollen. Wenn Sie einen Farbverlaufshintergrund 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. Allerdings wird das Material Surface durch ein Row ersetzt und es so formatiert, dass es das gewünschte Erscheinungsbild erreicht.

Wenn Sie Material-Konzepte überhaupt nicht verwenden möchten, z. B. wenn Sie Ihr eigenes maßgeschneidertes Designsystem erstellen, können Sie auf die ausschließliche Verwendung von Fundament-Layer-Komponenten umsteigen:

@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 höchsten Ebene. 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

Die Philosophie von Compose zum Erstellen von mehrschichtigen, wiederverwendbaren Komponenten bedeutet, dass Sie nicht immer nach den Bausteinen der unteren Ebenen greifen sollten. Viele Komponenten auf höherer Ebene bieten nicht nur mehr Funktionen, sondern implementieren häufig Best Practices wie die Unterstützung der Barrierefreiheit.

Wenn Sie beispielsweise Ihrer benutzerdefinierten Komponente die Unterstützung von Touch-Gesten hinzufügen möchten, können Sie dies mit Modifier.pointerInput von Grund auf neu erstellen. Es gibt aber auch andere übergeordnete Komponenten, die einen besseren Ausgangspunkt bieten, z. B. Modifier.draggable, Modifier.scrollable oder Modifier.swipeable.

In der Regel sollten Sie 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.