Benutzerdefinierte Designsysteme in Compose

Material ist zwar unser empfohlenes Designsystem und Jetpack Compose liefert eine Implementierung von Material aus, Sie sind aber nicht gezwungen, es zu verwenden. Material baut vollständig auf öffentlichen APIs auf, sodass Sie auf dieselbe Weise Ihr eigenes Designsystem erstellen können.

Dafür gibt es mehrere Ansätze:

Möglicherweise möchten Sie Material-Komponenten auch weiterhin mit einem benutzerdefinierten Designsystem verwenden. Das ist möglich, aber es gibt einige Dinge zu beachten, die zu Ihrem Ansatz passen.

Weitere Informationen zu den untergeordneten Konstrukten und APIs, die von MaterialTheme und benutzerdefinierten Designsystemen verwendet werden, finden Sie in der Anleitung Aufbau eines Designs in Compose.

Materialdesigns erweitern

Compose Material modelliert Material Theming genau, damit die Einhaltung der Material-Richtlinien einfach und typsicher ist. Es ist jedoch möglich, die Farben, Typografie und Formsätze mit zusätzlichen Werten zu erweitern.

Die einfachste Methode ist das Hinzufügen von Erweiterungseigenschaften:

// Use with MaterialTheme.colors.snackbarAction
val Colors.snackbarAction: Color
    get() = if (isLight) Red300 else Red700

// Use with MaterialTheme.typography.textFieldInput
val Typography.textFieldInput: TextStyle
    get() = TextStyle(/* ... */)

// Use with MaterialTheme.shapes.card
val Shapes.card: Shape
    get() = RoundedCornerShape(size = 20.dp)

Dies sorgt für Konsistenz mit MaterialTheme-Nutzungs-APIs. Ein Beispiel hierfür ist primarySurface, das in Compose definiert wird und als Proxy zwischen primary und surface je nach Colors.isLight fungiert.

Ein weiterer Ansatz besteht darin, ein erweitertes Thema zu definieren, das MaterialTheme und seine Werte „umschließt“.

Angenommen, Sie möchten zwei zusätzliche Farben – tertiary und onTertiary – hinzufügen und die vorhandenen Materialfarben beibehalten:

@Immutable
data class ExtendedColors(
    val tertiary: Color,
    val onTertiary: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        tertiary = Color.Unspecified,
        onTertiary = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        tertiary = Color(0xFFA8EFF0),
        onTertiary = Color(0xFF002021)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

// Use with eg. ExtendedTheme.colors.tertiary
object ExtendedTheme {
    val colors: ExtendedColors
        @Composable
        get() = LocalExtendedColors.current
}

Dies ähnelt den Nutzungs-APIs von MaterialTheme. Außerdem unterstützt es mehrere Designs, da Sie ExtendedThemes auf die gleiche Weise wie MaterialTheme verschachteln können.

Materialkomponenten verwenden

Beim Erweitern von Material Theming werden vorhandene MaterialTheme-Werte beibehalten und Materialkomponenten haben weiterhin angemessene Standardwerte.

Wenn Sie erweiterte Werte in Komponenten verwenden möchten, müssen Sie sie in eigene zusammensetzbare Funktionen einbinden. Dabei legen Sie die zu ändernden Werte direkt fest und stellen andere Werte als Parameter für die entsprechende zusammensetzbare Funktion bereit:

@Composable
fun ExtendedButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = ExtendedTheme.colors.tertiary,
            contentColor = ExtendedTheme.colors.onTertiary
            /* Other colors use values from MaterialTheme */
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Gegebenenfalls würden Sie dann Button durch ExtendedButton ersetzen.

@Composable
fun ExtendedApp() {
    ExtendedTheme {
        /*...*/
        ExtendedButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

Materialsysteme austauschen

Anstatt Material Theming zu erweitern, können Sie ein oder mehrere Systeme (Colors, Typography oder Shapes) durch eine benutzerdefinierte Implementierung ersetzen und die anderen Systeme beibehalten.

Angenommen, Sie möchten die Schrift- und Formsysteme ersetzen und das Farbsystem beibehalten:

@Immutable
data class ReplacementTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class ReplacementShapes(
    val component: Shape,
    val surface: Shape
)

val LocalReplacementTypography = staticCompositionLocalOf {
    ReplacementTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalReplacementShapes = staticCompositionLocalOf {
    ReplacementShapes(
        component = RoundedCornerShape(ZeroCornerSize),
        surface = RoundedCornerShape(ZeroCornerSize)
    )
}

@Composable
fun ReplacementTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val replacementTypography = ReplacementTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val replacementShapes = ReplacementShapes(
        component = RoundedCornerShape(percent = 50),
        surface = RoundedCornerShape(size = 40.dp)
    )
    CompositionLocalProvider(
        LocalReplacementTypography provides replacementTypography,
        LocalReplacementShapes provides replacementShapes
    ) {
        MaterialTheme(
            /* colors = ... */
            content = content
        )
    }
}

// Use with eg. ReplacementTheme.typography.body
object ReplacementTheme {
    val typography: ReplacementTypography
        @Composable
        get() = LocalReplacementTypography.current
    val shapes: ReplacementShapes
        @Composable
        get() = LocalReplacementShapes.current
}

Materialkomponenten verwenden

Wenn eines oder mehrere MaterialTheme-Systeme ersetzt wurden, kann die unveränderte Verwendung von Material-Komponenten zu unerwünschten Farbe, Typ oder Form von Material führen.

Wenn Sie Ersatzwerte in Komponenten verwenden möchten, binden Sie sie in Ihre eigenen zusammensetzbaren Funktionen ein, legen Sie die Werte direkt für das relevante System fest und stellen Sie andere Werte als Parameter für die enthaltene zusammensetzbare Funktion bereit.

@Composable
fun ReplacementButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        shape = ReplacementTheme.shapes.component,
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = ReplacementTheme.typography.body
            ) {
                content()
            }
        }
    )
}

Gegebenenfalls würden Sie dann Button durch ReplacementButton ersetzen.

@Composable
fun ReplacementApp() {
    ReplacementTheme {
        /*...*/
        ReplacementButton(onClick = { /* ... */ }) {
            /* ... */
        }
    }
}

Implementierung eines vollständig benutzerdefinierten Designsystems

Möglicherweise möchten Sie Material Theming durch ein vollständig benutzerdefiniertes Designsystem ersetzen. MaterialTheme stellt die folgenden Systeme bereit:

  • Colors, Typography und Shapes: Material-Design-Systeme
  • ContentAlpha: Deckkraft zur Vermittlung der Betonung in Text und Icon
  • TextSelectionColors: Farben, die für die Textauswahl durch Text und TextField verwendet werden
  • Ripple und RippleTheme: Material-Implementierung von Indication

Wenn Sie weiterhin Material-Komponenten verwenden möchten, müssen Sie einige dieser Systeme in Ihrem benutzerdefinierten Design oder Ihren benutzerdefinierten Designs ersetzen oder die Systeme in Ihren Komponenten verarbeiten, um unerwünschtes Verhalten zu vermeiden.

Designsysteme sind jedoch nicht auf die Konzepte beschränkt, auf denen Material basiert. Sie können vorhandene Systeme ändern und komplett neue Systeme mit neuen Klassen und Typen einführen, um andere Konzepte mit Themen kompatibel zu machen.

Im folgenden Code modellieren wir ein benutzerdefiniertes Farbsystem, das Farbverläufe (List<Color>) enthält und ein Typsystem enthält. Außerdem wird ein neues Höhensystem eingeführt und andere von MaterialTheme bereitgestellte Systeme ausgeschlossen:

@Immutable
data class CustomColors(
    val content: Color,
    val component: Color,
    val background: List<Color>
)

@Immutable
data class CustomTypography(
    val body: TextStyle,
    val title: TextStyle
)

@Immutable
data class CustomElevation(
    val default: Dp,
    val pressed: Dp
)

val LocalCustomColors = staticCompositionLocalOf {
    CustomColors(
        content = Color.Unspecified,
        component = Color.Unspecified,
        background = emptyList()
    )
}
val LocalCustomTypography = staticCompositionLocalOf {
    CustomTypography(
        body = TextStyle.Default,
        title = TextStyle.Default
    )
}
val LocalCustomElevation = staticCompositionLocalOf {
    CustomElevation(
        default = Dp.Unspecified,
        pressed = Dp.Unspecified
    )
}

@Composable
fun CustomTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val customColors = CustomColors(
        content = Color(0xFFDD0D3C),
        component = Color(0xFFC20029),
        background = listOf(Color.White, Color(0xFFF8BBD0))
    )
    val customTypography = CustomTypography(
        body = TextStyle(fontSize = 16.sp),
        title = TextStyle(fontSize = 32.sp)
    )
    val customElevation = CustomElevation(
        default = 4.dp,
        pressed = 8.dp
    )
    CompositionLocalProvider(
        LocalCustomColors provides customColors,
        LocalCustomTypography provides customTypography,
        LocalCustomElevation provides customElevation,
        content = content
    )
}

// Use with eg. CustomTheme.elevation.small
object CustomTheme {
    val colors: CustomColors
        @Composable
        get() = LocalCustomColors.current
    val typography: CustomTypography
        @Composable
        get() = LocalCustomTypography.current
    val elevation: CustomElevation
        @Composable
        get() = LocalCustomElevation.current
}

Materialkomponenten verwenden

Wenn kein MaterialTheme vorhanden ist, führt die unveränderte Verwendung von Material-Komponenten zu unerwünschten Farbe, Typ, Form und Anzeigeverhalten von Material.

Wenn Sie benutzerdefinierte Werte in Komponenten verwenden möchten, müssen Sie sie in Ihre eigenen zusammensetzbaren Funktionen einbinden. Dabei legen Sie die Werte direkt für das relevante System fest und stellen andere Werte als Parameter für die enthaltende zusammensetzbare Funktion zur Verfügung.

Wir empfehlen Ihnen, auf Werte zuzugreifen, die Sie in Ihrem benutzerdefinierten Design festgelegt haben. Wenn Ihr Theme Color, TextStyle, Shape oder andere Systeme nicht unterstützt, können Sie diese auch hartcodieren.

@Composable
fun CustomButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    content: @Composable RowScope.() -> Unit
) {
    Button(
        colors = ButtonDefaults.buttonColors(
            containerColor = CustomTheme.colors.component,
            contentColor = CustomTheme.colors.content,
            disabledContainerColor = CustomTheme.colors.content
                .copy(alpha = 0.12f)
                .compositeOver(CustomTheme.colors.component),
            disabledContentColor = CustomTheme.colors.content
                .copy(alpha = ContentAlpha.disabled)
        ),
        shape = ButtonShape,
        elevation = ButtonDefaults.elevatedButtonElevation(
            defaultElevation = CustomTheme.elevation.default,
            pressedElevation = CustomTheme.elevation.pressed
            /* disabledElevation = 0.dp */
        ),
        onClick = onClick,
        modifier = modifier,
        content = {
            ProvideTextStyle(
                value = CustomTheme.typography.body
            ) {
                content()
            }
        }
    )
}

val ButtonShape = RoundedCornerShape(percent = 50)

Wenn Sie neue Klassentypen eingeführt haben, z. B. List<Color> zur Darstellung von Farbverläufen, ist es möglicherweise besser, die Komponenten komplett neu zu implementieren, anstatt sie zu umschließen. Sehen Sie sich beispielsweise JetsnackButton aus dem Jetsnack-Beispiel an.