Sistemas de diseño personalizado en Compose

Si bien recomendamos el sistema de diseño Material y Jetpack Compose viene con una implementación incluida, no es obligatorio que lo uses. Material está íntegramente compilado sobre APIs públicas, por lo que es posible que crees tu propio sistema de diseño de la misma manera.

Puedes adoptar varios enfoques:

También te recomendamos que continúes usando componentes de Material con un sistema de diseño personalizado. Es posible hacerlo, pero debes tener en cuenta algunos aspectos para adaptar el enfoque que elegiste.

Para obtener más información sobre las construcciones de nivel inferior y las APIs que usan MaterialTheme y los sistemas de diseño personalizado, consulta la guía Anatomía de un tema en Compose.

Cómo extender temas de Material

Compose Material modela cuidadosamente los temas de Material para que los lineamientos correspondientes sean simples y seguros de seguir. Sin embargo, es posible extender el color, la tipografía y los conjuntos de formas con valores adicionales.

El enfoque más simple es agregar propiedades de extensión:

// 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)

De esta manera, se brinda coherencia con las APIs de uso de MaterialTheme. Un ejemplo de esto definido por el mismo Compose es primarySurface, que actúa como un proxy entre primary y surface, según Colors.isLight.

Otro enfoque es definir un tema extendido que "una" MaterialTheme y sus valores.

Supongamos que deseas agregar dos colores adicionales, tertiary y onTertiary, a la vez que mantienes los colores existentes de Material:

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

Es similar a las APIs de uso de MaterialTheme. También admite varios temas, ya que puedes anidar objetos ExtendedTheme de la misma manera que MaterialTheme.

Cómo usar componentes de Material

Cuando se extienden los temas de Material, se mantienen los valores MaterialTheme existentes, y los componentes de Material continúan teniendo valores predeterminados razonables.

Si deseas usar valores extendidos en los componentes, únelos en tus propias funciones de componibilidad, configura directamente los valores que deseas modificar y expón otros como parámetros al elemento componible que los contiene:

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

Luego, reemplaza los usos de Button por ExtendedButton cuando corresponda.

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

Cómo reemplazar sistemas de Material

En lugar de extender los temas de Material, te recomendamos que reemplaces uno o más sistemas (Colors, Typography o Shapes) por una implementación personalizada, a la vez que mantienes los otros.

Supongamos que deseas reemplazar los sistemas de tipos y formas mientras mantienes el sistema de colores:

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

Cómo usar componentes de Material

Cuando se reemplazan uno o más sistemas de MaterialTheme, es posible que usar los componentes de Material tal como están genere valores de color, tipo o forma de Material no deseados.

Si deseas usar valores de reemplazo en los componentes, únelos en tus propias funciones de componibilidad, configura directamente los valores para el sistema relevante y expón otros como parámetros al elemento componible que los contiene.

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

Luego, reemplaza los usos de Button por ReplacementButton cuando corresponda.

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

Cómo implementar un sistema de diseño totalmente personalizado

Te recomendamos que reemplaces los temas de Material por un sistema de diseño totalmente personalizado. Ten en cuenta que MaterialTheme brinda los siguientes sistemas:

  • Colors, Typography y Shapes: Sistemas de temas de Material
  • ContentAlpha: Niveles de opacidad para marcar énfasis en Text y Icon
  • TextSelectionColors: Colores que usa Text y TextField para seleccionar texto
  • Ripple y RippleTheme: Implementación de Material de Indication

Si deseas continuar usando componentes de Material, deberás reemplazar algunos de estos sistemas en los temas personalizados o controlar los sistemas en los componentes para evitar un comportamiento no deseado.

Sin embargo, los sistemas de diseño no se limitan a los conceptos en los que se basa Material. Puedes modificar los sistemas existentes y agregar sistemas completamente nuevos, con clases y tipos nuevos, para que otros conceptos sean compatibles con los temas.

En el siguiente código, modelamos un sistema de colores personalizado que incluye gradientes (List<Color>), incluimos un sistema de tipos, agregamos un sistema nuevo de elevación y excluimos otros sistemas que brinda MaterialTheme:

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

Cómo usar componentes de Material

Cuando ningún objeto MaterialTheme está presente, usar los componentes de Material tal como están generará valores de color, tipo y forma de Material no deseados, así como comportamiento de indicación.

Si deseas usar valores personalizados en los componentes, únelos en tus propias funciones de componibilidad, configura directamente los valores para el sistema relevante y expón otros como parámetros al elemento componible que los contiene.

Te recomendamos que accedas a los valores que establezcas desde el tema personalizado. Como alternativa, si el tema no brinda Color, TextStyle, Shape ni otros sistemas, puedes codificarlos.

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

Si agregaste nuevos tipos de clases, como List<Color> para representar gradientes, es posible que sea mejor implementar componentes desde cero en lugar de unirlos. Por ejemplo, observa JetsnackButton del ejemplo de Jetsnack.