Benutzerdefinierte Designsysteme in Compose

Material ist unser empfohlenes Designsystem. Jetpack Compose enthält eine Implementierung von Material, aber Sie müssen es nicht verwenden. Material basiert vollständig auf öffentlichen APIs. Sie können also auf dieselbe Weise Ihr eigenes Designsystem erstellen.

Es gibt verschiedene Ansätze:

Möglicherweise möchten Sie auch Material-Komponenten mit einem benutzerdefinierten Designsystem verwenden. Das ist möglich, aber Sie müssen einige Dinge beachten, um den gewählten Ansatz zu berücksichtigen.

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

Material Theming erweitern

Compose Material ist eng an Material Theming angelehnt, damit die Richtlinien für Material einfach und typsicher eingehalten werden können. Sie können die Farb-, Typografie- und Formensets jedoch mit zusätzlichen Werten erweitern. Der einfachste Ansatz besteht darin, Erweiterungseigenschaften hinzuzufügen:

// Use with MaterialTheme.colorScheme.snackbarAction
val ColorScheme.snackbarAction: Color
    @Composable
    get() = if (isSystemInDarkTheme()) 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)

Dadurch wird die Konsistenz mit den APIs für die Verwendung von MaterialTheme gewährleistet. Ein Beispiel dafür, das von Compose selbst definiert wird, ist surfaceColorAtElevation. Damit wird die Oberflächenfarbe festgelegt, die je nach Höhe verwendet werden soll.

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

Angenommen, Sie möchten zwei zusätzliche Farben hinzufügen: caution und onCaution. Das ist eine gelbe Farbe, die für Aktionen verwendet wird, die halb gefährlich sind. Die vorhandenen Material-Farben sollen beibehalten werden:

@Immutable
data class ExtendedColors(
    val caution: Color,
    val onCaution: Color
)

val LocalExtendedColors = staticCompositionLocalOf {
    ExtendedColors(
        caution = Color.Unspecified,
        onCaution = Color.Unspecified
    )
}

@Composable
fun ExtendedTheme(
    /* ... */
    content: @Composable () -> Unit
) {
    val extendedColors = ExtendedColors(
        caution = Color(0xFFFFCC02),
        onCaution = Color(0xFF2C2D30)
    )
    CompositionLocalProvider(LocalExtendedColors provides extendedColors) {
        MaterialTheme(
            /* colors = ..., typography = ..., shapes = ... */
            content = content
        )
    }
}

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

Das ähnelt den APIs für die Verwendung von MaterialTheme. Es werden auch mehrere Designs unterstützt, da Sie ExtendedThemes auf dieselbe Weise wie MaterialTheme verschachteln können.

Material-Komponenten verwenden

Wenn Sie Material Theming erweitern, bleiben die vorhandenen MaterialTheme-Werte erhalten und Material-Komponenten haben weiterhin sinnvolle Standardwerte.

Wenn Sie erweiterte Werte in Komponenten verwenden möchten, umschließen Sie sie in Ihren eigenen zusammensetzbaren Funktionen. Legen Sie die Werte, die Sie ändern möchten, direkt fest und machen Sie andere als Parameter für die enthaltende zusammensetzbare Funktion verfügbar:

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

Anschließend ersetzen Sie die Verwendungen von Button gegebenenfalls durch ExtendedButton.

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

Material-Subsysteme ersetzen

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

Angenommen, Sie möchten die Typografie- und Formensysteme ersetzen, aber 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
}

Material-Komponenten verwenden

Wenn ein oder mehrere Systeme von MaterialTheme ersetzt wurden, kann die unveränderte Verwendung von Material-Komponenten zu unerwünschten Material-Farb-, Typografie- oder Formwerten führen.

Wenn Sie Ersatzwerte in Komponenten verwenden möchten, umschließen Sie sie in Ihren eigenen zusammensetzbaren Funktionen. Legen Sie die Werte für das relevante System direkt fest und machen Sie andere als Parameter für die enthaltende zusammensetzbare Funktion verfügbar.

@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()
            }
        }
    )
}

Anschließend ersetzen Sie die Verwendungen von Button gegebenenfalls durch ReplacementButton wo angebracht.

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

Vollständig benutzerdefiniertes Designsystem implementieren

Möglicherweise möchten Sie Material Theming durch ein vollständig benutzerdefiniertes Designsystem ersetzen. Beachten Sie, dass MaterialTheme die folgenden Systeme bietet:

  • Colors, Typography und Shapes: Material Theming-Systeme
  • TextSelectionColors: Farben, die für die Textauswahl durch Text und TextField verwendet werden
  • Ripple und RippleTheme: Material-Implementierung von Indication

Wenn Sie Material-Komponenten weiterhin verwenden möchten, müssen Sie einige dieser Systeme in 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 die sich Material stützt. Sie können vorhandene Systeme ändern und völlig neue einführen – mit neuen Klassen und Typen –, um andere Konzepte mit Designs kompatibel zu machen.

Im folgenden Code modellieren wir ein benutzerdefiniertes Farbsystem, das Farbverläufe (List<Color>) enthält, ein Typografiesystem einführt, ein neues Höhensystem einführt, und andere Systeme ausschließt, die von MaterialTheme bereitgestellt werden:

Screenshot einer Benutzeroberfläche einer mobilen App, die ein benutzerdefiniertes Designsystem mit Elementen zeigt, die Farbverläufe für Farben, benutzerdefinierte Typografie und Höhe verwenden.

@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
}

Material-Komponenten verwenden

Wenn kein MaterialTheme vorhanden ist, führt die unveränderte Verwendung von Material-Komponenten zu unerwünschten Material-Farb-, Typografie- und Formwerten sowie zu unerwünschtem Indikationsverhalten.

Wenn Sie benutzerdefinierte Werte in Komponenten verwenden möchten, umschließen Sie sie in Ihren eigenen zusammensetzbaren Funktionen. Legen Sie die Werte für das relevante System direkt fest und machen Sie andere als Parameter für die enthaltende zusammensetzbare Funktion verfügbar.

Wir empfehlen, auf die von Ihnen festgelegten Werte aus Ihrem benutzerdefinierten Design zuzugreifen. Alternativ können Sie Color, TextStyle, Shape oder andere Systeme fest codieren, wenn Ihr Design sie nicht bereitstellt.

@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 = 0.38f)

        ),
        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> für Farbverläufe, ist es möglicherweise besser, Komponenten von Grund auf neu zu implementieren, anstatt sie zu umschließen. Ein Beispiel finden Sie unter JetsnackButton im Jetsnack-Beispiel.