Designs mit Stilen

Es gibt mehrere Möglichkeiten, Ihre Apps mit Designs zu erstellen. Die Wahl hängt davon ab, wie Ihre App im Hinblick auf die Einführung von Material Design positioniert ist:

  1. Vollständig benutzerdefiniertes Designsystem ohne Material Design
    • Empfehlung: Definieren Sie Komponentenstile, die Werte aus dem Design verwenden, und stellen Sie Stilparameter für Komponenten des Designsystems bereit.
  2. Material Design verwenden
    • Empfehlung: Warten Sie auf die Einführung von Material, um es in Designs zu integrieren. Verwenden Sie nach Möglichkeit Designs für Ihre eigenen Komponenten.

Die Designebene

Im herkömmlichen Compose-Modell basiert die Anpassung oft stark auf dem Überschreiben globaler Tokens (Farben und Typografie), die von MaterialTheme bereitgestellt werden, oder auf dem Umschließen und Überschreiben von Eigenschaften einer zusammensetzbaren Komponente des Designsystems, sofern möglich. Manchmal gibt es Eigenschaften in der Material-Ebene, die nicht über die Subsysteme oder Parameter verfügbar gemacht werden, sondern als hartcodierte Standardwerte in der Komponente selbst enthalten sind.

Mit der Styles API gibt es eine neue Abstraktionsebene, die eine Brücke zwischen Subsystemen und Komponenten bildet: Designs.

Layer Verantwortung Beispiel
Werte für das Subsystem Benannte Werte val Primary = Color(0xFF34A85E)
Atomare Designs Design, das genau eine Eigenschaft ändert val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic
Komponentendesigns Komponentenspezifische Konfigurationen Eine Schaltfläche mit primärem Hintergrund und 16 dp-Abstand. val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
Komponenten Das funktionale UI-Element, das ein Design verwendet. Button(style = buttonStyle) { ... }
Diagramm zum Theming mit Stilen mit der neuen Ebene
Abbildung 1. Ein Beispiel für eine Komponente und wie sie auf Designs aus einem Design zugreift.

Atomare Designs im Vergleich zu monolithischen Designs

Mit der Styles API können Sie ein Design in separate atomare Designs aufteilen. Anstatt komplexe, komponentenspezifische Designs wie baseButtonStyle zu definieren, können Sie auch kleine, zweckgebundene Hilfsdesigns erstellen. Diese fungieren als Ihre „Atome“.

// Define single-purpose "atomic" styles
val paddingAtomic = Style {
    contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
    shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
    background(Color.Blue)
}
val largeSizeAtomic = Style {
    size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
    hovered {
        animate {
            dropShadow(
                Shadow(
                    offset = DpOffset(
                        0.dp,
                        0.dp
                    ),
                    radius = 2.dp,
                    spread = 0.dp,
                    color = Color.Blue,
                )
            )
        }
    }
}

Zusammensetzung mit „then“

Eine der leistungsstarken Funktionen der neuen Styles API ist der Operator then, mit dem Sie mehrere Style-Objekte zusammenführen können. So können Sie eine Komponente mit atomaren Hilfsklassen erstellen.

Traditionell (nicht atomar):

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

Atomare Umgestaltung:

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

Designs in Ihrem Designsystem übernehmen

Je nachdem, wo sich Ihr Designsystem im Spektrum befindet, haben Sie folgende Möglichkeiten, Designs in Ihr Designsystem zu übernehmen.

Benutzerdefiniertes Designsystem mit Designs

In folgenden Fällen zu empfehlen: Sie haben einen umfassenden Markenleitfaden erhalten, der nicht auf Material Design basiert, und Sie planen nicht, Material Design zu verwenden.

Strategie: Implementieren Sie ein vollständig benutzerdefiniertes Designsystem und stellen Sie Designs als Teil des Designs bereit.

Diese Option ist der benutzerdefinierte Pfad, wenn Sie Material nicht als Hauptsprache für Ihr Designsystem verwenden. Sie umgehen MaterialTheme vollständig für visuelle Definitionen und haben bereits ein eigenes benutzerdefiniertes Design erstellt. Sie erstellen ein CompanyTheme, das als Container für Ihre Designs dient.

  • Funktionsweise: Erstellen Sie ein CompanyTheme-Objekt, das Style-Objekte für jede Komponente in Ihrem System enthält. Ihre Komponenten (entweder Wrapper um die Material-Logik oder benutzerdefinierte Box- oder Layout-Implementierungen) verwenden diese Stile direkt und stellen einen Style-Parameter für Nutzer Ihres Designsystems bereit.
  • Die Designebene: Designs sind die primäre Definition Ihres Design systems. Tokens sind benannte Variablen, die in diese Designs eingefügt werden. Dies ermöglicht eine umfassende Anpassung, z. B. das Definieren eindeutiger Animationen für Statusänderungen (z. B. Animieren von Skalierung und Farbe beim Drücken).

Wenn Sie ein eigenes benutzerdefiniertes Design ohne Material erstellen und Designs übernehmen möchten, fügen Sie Ihrem Design eine Liste von Designs hinzu. So können Sie von überall in Ihrem Projekt auf Ihre Basisdesigns zugreifen.

  1. Erstellen Sie eine Styles-Klasse, in der die verschiedenen Designs in Ihrer Anwendung gespeichert werden, und erstellen Sie die Standardwerte. In der Jetsnack-App heißt die Klasse beispielsweise JetsnackStyles:

    object JetsnackStyles{
        val buttonStyle: Style = Style {
            shape(shapes.medium)
            background(colors.brand)
            contentColor(colors.textPrimary)
            contentPaddingVertical(8.dp)
            contentPaddingHorizontal(24.dp)
            textStyle(typography.labelLarge)
            disabled {
                animate {
                    background(colors.brandSecondary)
                }
            }
        }
        val cardStyle: Style = Style {
            shape(shapes.medium)
            background(colors.uiBackground)
            contentColor(colors.textPrimary)
        }
    }

  2. Stellen Sie Styles als Teil Ihres Gesamtdesigns bereit und stellen Sie Hilfs-Erweiterungsfunktionen für StyleScope bereit, um auf die Subsysteme zuzugreifen:

    @Immutable
    class JetsnackTheme(
        val colors: JetsnackColors = LightJetsnackColors,
        val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
        val shapes: Shapes = Shapes()
    ) {
        companion object {
            val colors: JetsnackColors
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.colors
    
            val typography: androidx.compose.material3.Typography
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.typography
    
            val shapes: Shapes
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.shapes
    
            val styles: JetsnackStyles = JetsnackStyles
    
            val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
                get() = LocalJetsnackThemeInstance
        }
    }
    
    val StyleScope.colors: JetsnackColors
        get() = LocalJetsnackTheme.currentValue.colors
    
    val StyleScope.typography: androidx.compose.material3.Typography
        get() = LocalJetsnackTheme.currentValue.typography
    
    val StyleScope.shapes: Shapes
        get() = LocalJetsnackTheme.currentValue.shapes
    
    internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
    
    @Composable
    fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
        val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
        val theme = JetsnackTheme(colors = colors)
    
        CompositionLocalProvider(
            LocalJetsnackTheme provides theme,
        ) {
            MaterialTheme(
                typography = LocalJetsnackTheme.current.typography,
                shapes = LocalJetsnackTheme.current.shapes,
                content = content,
            )
        }
    }

  3. Greifen Sie in Ihrer zusammensetzbaren Komponente auf JetsnackStyles zu:

    @Composable
    fun CustomButton(modifier: Modifier,
                     style: Style = Style,
                     text: String) {
        val interactionSource = remember { MutableInteractionSource() }
        val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    
        // Apply style to top level container in combination with incoming style from parameter.
        Box(modifier = modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                enabled = true,
                role = Role.Button,
                onClick = {
    
                },
            )
            .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
            Text(text)
        }
    }

Neben der globalen Designübernahme gibt es alternative Strategien, um Styles in Ihre Apps einzubinden. Sie können Styles inline für bestimmte Aufrufstellen verwenden oder statische Definitionen verwenden, wenn vollständige Designfunktionen nicht erforderlich sind. Styles sollten nur bedingt ausgetauscht werden, wenn sich das gesamte Design grundlegend unterscheidet. Sie sollten es vorziehen, auf dynamische Tokens innerhalb einer visuellen Definition zuzugreifen, anstatt zwischen verschiedenen Designobjekten zu wechseln.