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:
- Extiende
MaterialTheme
con valores de temas adicionales - Reemplaza uno o más sistemas Material (
Colors
,Typography
oShapes
) por implementaciones personalizadas, a la vez que mantienes los otros. - Implementa un sistema de diseño totalmente personalizado para reemplazar
MaterialTheme
.
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.
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 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.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 es surfaceColorAtElevation
, que determina el color de superficie que se debe usar según la elevación.
Otro enfoque es definir un tema extendido que "una" MaterialTheme
y sus valores.
Supongamos que deseas agregar dos colores adicionales, caution
y onCaution
, un color amarillo que se usa para acciones semipeligrosas, a la vez que mantienes los colores existentes de Material:
@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 quieres 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.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 = { /* ... */ }) { /* ... */ } } }
Reemplaza los subsistemas 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 y, al mismo tiempo, mantener 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 }
Usa 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
yShapes
: Sistemas de temas de MaterialTextSelectionColors
: Colores que usaText
yTextField
para seleccionar textoRipple
yRippleTheme
: Implementación de Material deIndication
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 }
Usa 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 = 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 gradientes, quizás sea mejor implementar componentes desde cero en lugar de unirlos. Por ejemplo, observa JetsnackButton
del ejemplo de Jetsnack.
Recomendaciones para ti
- Nota: El texto del vínculo se muestra cuando JavaScript está desactivado
- Material Design 3 en Compose
- Cómo migrar de Material 2 a Material 3 en Compose
- Anatomía de un tema en Compose