Benutzerdefinierte Designsysteme in Compose

Material ist unser empfohlenes Designsystem und Jetpack Compose liefert eine Implementierung von Material, aber du musst es nicht verwenden. Material basiert vollständig auf öffentlichen APIs, sodass es möglich ist, auf die gleiche Weise Ihr eigenes Designsystem zu erstellen.

Dafür gibt es mehrere Ansätze:

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

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

Material-Design erweitern

Beim Erstellen von Material werden Material-Designs sehr genau nachempfunden, um die Einhaltung der Material-Richtlinien zu erleichtern und typsicher zu machen. Es ist jedoch möglich, die Farb-, Typografie- und Formsätze mit zusätzlichen Werten zu erweitern.

Der einfachste Ansatz besteht darin, Erweiterungseigenschaften hinzuzufügen:

// 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 den MaterialTheme-Nutzungs-APIs. Ein Beispiel dafür ist primarySurface, das in Abhängigkeit von Colors.isLight als Proxy zwischen primary und surface fungiert.

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

Angenommen, Sie möchten zwei zusätzliche Farben hinzufügen – tertiary und onTertiary – 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 für MaterialTheme. Außerdem werden mehrere Designs unterstützt, da du ExtendedThemes auf dieselbe Weise wie MaterialTheme verschachteln kannst.

Material-Komponenten 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, schließen Sie sie 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.tertiary,
            contentColor = ExtendedTheme.colors.onTertiary
            /* Other colors use values from MaterialTheme */
        ),
        onClick = onClick,
        modifier = modifier,
        content = content
    )
}

Dann ersetzen Sie die Verwendungen von Button gegebenenfalls durch ExtendedButton.

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

Materialsysteme 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 Systeme 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
}

Material-Komponenten 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()
            }
        }
    )
}

Dann ersetzen Sie die Verwendungen von Button gegebenenfalls durch ReplacementButton.

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

Vollständig kundenspezifisches 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
  • ContentAlpha: Deckkraft zur Hervorhebung in Text und Icon
  • TextSelectionColors: Farben, die für die Textauswahl von Text und TextField verwendet werden
  • Ripple und RippleTheme: Materialisierte Implementierung von Indication

Wenn Sie weiterhin Material-Komponenten verwenden möchten, müssen Sie einige dieser Systeme in 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 denen das Material basiert. Sie können vorhandene Systeme ändern und ganz neue mit neuen Klassen und Typen einführen, um andere Konzepte mit Themen kompatibel zu machen.

Im folgenden Code wird ein benutzerdefiniertes Farbsystem mit Farbverläufen (List<Color>) modelliert, ein Typsystem eingebunden, 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
}

Material-Komponenten verwenden

Ist MaterialTheme nicht vorhanden, führt die unveränderte Verwendung von Material-Komponenten zu unerwünschten Farb-, Typ- und Formwerten sowie zum Anzeigeverhalten von Material.

Wenn Sie benutzerdefinierte Werte in Komponenten verwenden möchten, schließen Sie sie in Ihre eigenen zusammensetzbaren Funktionen ein, legen die Werte direkt für das relevante System fest und stellen Sie andere als Parameter an die enthaltene zusammensetzbare Funktion zur Verfügung.

Wir empfehlen, auf Werte zuzugreifen, die Sie in Ihrem benutzerdefinierten Design festgelegt haben. Wenn Ihr Design Color, TextStyle, Shape oder andere Systeme nicht unterstützt, 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 = 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 von Grund auf neu zu implementieren, anstatt sie zu umschließen. Sehen Sie sich als Beispiel JetsnackButton aus dem Jetsnack-Beispiel an.