Benutzerdefinierte Designsysteme in Compose

Material ist unser empfohlenes Designsystem und Jetpack Compose enthält eine Implementierung von Material. Sie müssen es aber nicht verwenden. Material Design basiert vollständig auf öffentlichen APIs. Daher ist es möglich, auf dieselbe Weise ein eigenes Designsystem zu erstellen.

Es gibt mehrere Möglichkeiten:

Sie können auch weiterhin Material-Komponenten mit einem benutzerdefinierten Designsystem verwenden. Das ist möglich, aber es gibt einige Dinge, die Sie beachten sollten, um den von Ihnen gewählten Ansatz zu unterstützen.

Weitere Informationen zu den Konstrukten und APIs der unteren Ebene, die von MaterialTheme und benutzerdefinierten Designsystemen verwendet werden, finden Sie im Leitfaden Anatomy of a theme in Compose (Anatomie eines Themas in Compose).

Material-Design erweitern

Compose Material orientiert sich stark an den Material-Design-Themen, damit die Material-Design-Richtlinien einfach und typsicher eingehalten werden können. Es ist jedoch möglich, die Farb-, Typografie- und Formsätze mit zusätzlichen Werten zu erweitern.

Am einfachsten ist es, 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)

Dies sorgt für Konsistenz mit den MaterialTheme-Nutzungs-APIs. Ein Beispiel hierfür ist surfaceColorAtElevation, das von Compose selbst definiert wird und die Oberflächenfarbe bestimmt, die je nach Höhe verwendet werden soll.

Eine weitere Möglichkeit 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, eine gelbe Farbe für Aktionen mit mittlerem Gefahrenpotenzial. Die vorhandenen Materialfarben sollen dabei 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
}

Dies ähnelt den Nutzungs-APIs für MaterialTheme. Außerdem werden mehrere Themen unterstützt, da ExtendedThemes genauso wie MaterialTheme verschachtelt werden können.

Materialkomponenten verwenden

Wenn Sie Material-Design-Themen erweitern, bleiben vorhandene MaterialTheme-Werte erhalten und Material-Komponenten haben weiterhin angemessene Standardwerte.

Wenn Sie erweiterte Werte in Komponenten verwenden möchten, schließen Sie diese in Ihre eigenen zusammensetzbaren Funktionen ein, legen Sie die zu ändernden Werte direkt fest und stellen Sie andere als Parameter für die enthaltene zusammensetzbare Funktion bereit:

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

Ersetzen Sie dann gegebenenfalls die Verwendung von Button durch ExtendedButton.

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

Material-Untersysteme 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 Typ- und Formsysteme ersetzen, das Farbsystem jedoch 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 Systeme von MaterialTheme ersetzt wurden, kann die Verwendung von Materialkomponenten in der vorliegenden Form zu unerwünschten Farb-, Typ- oder Formwerten für Material führen.

Wenn Sie Ersatzwerte in Komponenten verwenden möchten, schließen Sie diese in Ihre eigenen zusammensetzbaren Funktionen ein, legen die Werte direkt für das relevante System fest und stellen Sie andere 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()
            }
        }
    )
}

Ersetzen Sie dann gegebenenfalls die Verwendung von Button durch ReplacementButton.

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

Ein vollständig benutzerdefiniertes Designsystem implementieren

Sie können Material Theming durch ein vollständig benutzerdefiniertes Designsystem ersetzen. MaterialTheme bietet die folgenden Systeme:

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

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

Designsysteme sind jedoch nicht auf die Konzepte beschränkt, auf die Material Design basiert. Sie können vorhandene Systeme ändern und ganz 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 mit Farbverläufen (List<Color>), ein Typsystem, ein neues Höhensystem und schließen andere von MaterialTheme bereitgestellte Systeme aus:

@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 Verwendung von Materialkomponenten in der Standardeinstellung zu unerwünschten Materialfarben, -typen und -formwerten sowie zu unerwünschtem Anzeigeverhalten.

Wenn Sie benutzerdefinierte Werte in Komponenten verwenden möchten, verpacken Sie sie in Ihre eigenen composable-Funktionen, legen Sie die Werte direkt für das entsprechende System fest und geben Sie andere als Parameter für das enthaltene composable frei.

Wir empfehlen, auf Werte zuzugreifen, die Sie über Ihr benutzerdefiniertes Design festgelegt haben. Wenn Ihr Design keine Color-, TextStyle-, Shape- oder andere Systeme bietet, können Sie sie 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 = 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. Sehen Sie sich zum Beispiel JetsnackButton aus dem Jetsnack-Beispiel an.