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 puedes seguir usando componentes de Material con un diseño personalizado en un sistema de archivos. Es posible hacerlo, pero se deben tener en cuenta algunos aspectos para el enfoque que has adoptado.

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.

Extensión del tema 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 los conjuntos de color, tipografía y formas con de salida.

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

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

De esta manera, se brinda coherencia con las APIs de uso de MaterialTheme. Un ejemplo de esto definido por el mismo Compose surfaceColorAtElevation: que determina el color de superficie que se debe utilizar según la elevación.

Otro enfoque es definir un tema extendido que “envuelve” MaterialTheme y sus valores.

Supongamos que quieres agregar dos colores adicionales: caution y onCaution, un amarillo para acciones semipeligrosas, mientras se mantienen las Colores de Material existentes:

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

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 tu propia funciones de componibilidad, configurar directamente los valores que deseas modificar exponiendo 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.caution,
            contentColor = ExtendedTheme.colors.onCaution
            /* 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 subsistemas de Material

En lugar de extender los Temas de Material, puedes reemplazar uno o más sistemas (Colors, Typography o Shapes) con una implementación personalizada, mientras se mantienen las demás.

Supongamos que deseas reemplazar los sistemas de tipos y formas y, al mismo tiempo, mantener el color sistema:

@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 quieres usar valores de reemplazo en componentes, únelos en tu propia funciones de componibilidad, que establezcan directamente los valores para el sistema relevante exponiendo 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 = { /* ... */ }) {
            /* ... */
        }
    }
}

Implementa un sistema de diseño totalmente personalizado

Te recomendamos reemplazar los Temas de Material por un sistema de diseño completamente personalizado. Ten en cuenta que MaterialTheme brinda los siguientes sistemas:

  • Colors, Typography y Shapes: Sistemas de temas de Material
  • 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 tu propio elemento componible. funciones, establecer directamente los valores para el sistema relevante y exponer otros como parámetros del elemento componible que los contiene.

Te recomendamos que accedas a los valores que establezcas desde tu tema personalizado. Como alternativa, si tu tema no proporciona Color, TextStyle, Shape ni de 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 = 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)

Si introdujiste nuevos tipos de clases, como List<Color> para representar con gradientes, quizás sea mejor implementar componentes desde cero de envolverlos. Por ejemplo, echa un vistazo a JetsnackButton del ejemplo de Jetsnack.