Aplicación de temas con estilos

Hay varias formas de compilar tus apps con estilos. Lo que elijas dependerá de la posición de tu app en relación con su adopción de Material Design:

  1. Sistema de diseño totalmente personalizado, sin usar Material Design
    • Recomendación: Define estilos de componentes que consuman valores del tema y expongan parámetros de estilo en los componentes del sistema de diseño.
  2. Usa Material Design
    • Recomendación: Espera la adopción de Material para integrarlo con los estilos. Usa estilos en tus propios componentes cuando sea posible.

La capa de estilo

En el modelo tradicional de Compose, la personalización suele depender en gran medida de anular los tokens globales (colores y tipografía) que proporciona MaterialTheme, o bien de ajustar y anular las propiedades de un elemento componible del sistema de diseño cuando sea posible. A veces, hay propiedades dentro de la capa de Material que no se exponen a través de los subsistemas o parámetros, pero son valores predeterminados codificados de forma rígida en el componente.

Con la API de Styles, hay una nueva capa de abstracción que es un puente entre los subsistemas y los componentes: Styles.

Capa Responsabilidad Ejemplo
Valores del subsistema Valores con nombre val Primary = Color(0xFF34A85E)
Estilos atómicos Estilo que realiza exactamente un cambio de propiedad val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then largeSize then interactiveShadowAtomic
Estilos de componentes Configuraciones específicas de los componentes Un botón con fondo primario y relleno de 16 dp. val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
Componentes El elemento de la IU funcional que consume un estilo. Button(style = buttonStyle) { ... }
Diagrama que muestra la aplicación de temas con diseños y la introducción de la nueva capa
Figura 1. Un ejemplo de un componente y cómo accede a los estilos de un tema.

Estilos atómicos frente a monolíticos

Con la API de Styles, puedes dividir un estilo en estilos atómicos separados. En lugar de definir estilos complejos y específicos de los componentes, como baseButtonStyle, también puedes crear estilos de utilidad pequeños y de un solo propósito. Estos actúan como tus "átomos".

// Define single-purpose "atomic" styles
val paddingAtomic = Style {
    contentPadding(16.dp)
}
val roundedCornerShapeAtomic = Style {
    shape(RoundedCornerShape(8.dp))
}
val primaryBackgroundAtomic = Style {
    background(Color.Blue)
}
val largeSizeAtomic = Style {
    size(100.dp, 40.dp)
}
val interactiveShadowAtomic = Style {
    hovered {
        animate {
            dropShadow(
                Shadow(
                    offset = DpOffset(
                        0.dp,
                        0.dp
                    ),
                    radius = 2.dp,
                    spread = 0.dp,
                    color = Color.Blue,
                )
            )
        }
    }
}

Composición con "then"

Una de las potentes funciones de la nueva API de Styles es el operador then, que te permite combinar varios objetos Style. Esto te permite compilar un componente con clases de utilidad atómicas.

Tradicional (no atómico):

// One large monolithic style
val buttonStyle = Style {
    contentPadding(16.dp)
    shape(RoundedCornerShape(8.dp))
    background(Color.Blue)
}

Refactorización atómica:

// Combine atoms to create the final appearance
val buttonStyle = paddingAtomic then roundedCornerShapeAtomic then primaryBackgroundAtomic then interactiveShadowAtomic

Adopta estilos en tu sistema de diseño

Ten en cuenta las siguientes opciones cuando adoptes estilos en tu sistema de diseño, según la posición de tu sistema de diseño en el espectro.

Sistema de diseño personalizado con estilos

Considera cuándo: Te entregaron una guía de marca extensa que no está basada en Material Design y no planeas usar Material Design.

Estrategia: Implementa un sistema de diseño totalmente personalizado y expón los estilos como parte del tema.

Esta opción es la ruta de acceso personalizada si no usas Material como el idioma principal de tu sistema de diseño. Omites MaterialTheme por completo para las definiciones visuales y ya creaste tu propio tema personalizado. Compilas un CompanyTheme que actúa como un contenedor para tus estilos.

  • Cómo funciona: Crea un objeto CompanyTheme que contenga objetos Style para cada componente de tu sistema. Tus componentes (ya sean wrappers alrededor de la lógica de Material o implementaciones personalizadas de Box o Layout) consumen estos estilos directamente y exponen un parámetro Style para los consumidores de tu sistema de diseño.
  • La capa de estilo: Los estilos son la definición principal de tu sistema de diseño. Los tokens son variables con nombre que se introducen en estos estilos. Esto permite una personalización profunda, como definir animaciones únicas para los cambios de estado (por ejemplo, animar la escala y el color al presionar).

Si compilas tu propio tema personalizado sin usar Material y quieres adoptar estilos, agrega tu lista de estilos a tu tema. Esto te permite acceder a tus estilos base desde cualquier lugar de tu proyecto.

  1. Crea una clase Styles que almacene los distintos estilos de tu aplicación y cree los valores predeterminados. Por ejemplo, en la app de Jetsnack, la clase se llama JetsnackStyles:

    object JetsnackStyles{
        val buttonStyle: Style = Style {
            shape(shapes.medium)
            background(colors.brand)
            contentColor(colors.textPrimary)
            contentPaddingVertical(8.dp)
            contentPaddingHorizontal(24.dp)
            textStyle(typography.labelLarge)
            disabled {
                animate {
                    background(colors.brandSecondary)
                }
            }
        }
        val cardStyle: Style = Style {
            shape(shapes.medium)
            background(colors.uiBackground)
            contentColor(colors.textPrimary)
        }
    }

  2. Proporciona Styles como parte de tu tema general y expón funciones de extensión auxiliares en StyleScope para acceder a los subsistemas:

    @Immutable
    class JetsnackTheme(
        val colors: JetsnackColors = LightJetsnackColors,
        val typography: androidx.compose.material3.Typography = androidx.compose.material3.Typography(),
        val shapes: Shapes = Shapes()
    ) {
        companion object {
            val colors: JetsnackColors
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.colors
    
            val typography: androidx.compose.material3.Typography
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.typography
    
            val shapes: Shapes
                @Composable @ReadOnlyComposable
                get() = LocalJetsnackTheme.current.shapes
    
            val styles: JetsnackStyles = JetsnackStyles
    
            val LocalJetsnackTheme: ProvidableCompositionLocal<JetsnackTheme>
                get() = LocalJetsnackThemeInstance
        }
    }
    
    val StyleScope.colors: JetsnackColors
        get() = LocalJetsnackTheme.currentValue.colors
    
    val StyleScope.typography: androidx.compose.material3.Typography
        get() = LocalJetsnackTheme.currentValue.typography
    
    val StyleScope.shapes: Shapes
        get() = LocalJetsnackTheme.currentValue.shapes
    
    internal val LocalJetsnackThemeInstance = staticCompositionLocalOf { JetsnackTheme() }
    
    @Composable
    fun JetsnackTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
        val colors = if (darkTheme) DarkJetsnackColors else LightJetsnackColors
        val theme = JetsnackTheme(colors = colors)
    
        CompositionLocalProvider(
            LocalJetsnackTheme provides theme,
        ) {
            MaterialTheme(
                typography = LocalJetsnackTheme.current.typography,
                shapes = LocalJetsnackTheme.current.shapes,
                content = content,
            )
        }
    }

  3. Accede a JetsnackStyles dentro de tu elemento componible:

    @Composable
    fun CustomButton(modifier: Modifier,
                     style: Style = Style,
                     text: String) {
        val interactionSource = remember { MutableInteractionSource() }
        val styleState = remember(interactionSource) { MutableStyleState(interactionSource) }
    
        // Apply style to top level container in combination with incoming style from parameter.
        Box(modifier = modifier
            .clickable(
                interactionSource = interactionSource,
                indication = null,
                enabled = true,
                role = Role.Button,
                onClick = {
    
                },
            )
            .styleable(styleState, JetsnackTheme.styles.buttonStyle, style)) {
            Text(text)
        }
    }

Además de la adopción de temas globales, existen estrategias alternativas para incorporar Styles en tus apps. Puedes aprovechar Styles en línea para sitios de llamada específicos o usar definiciones estáticas cuando no sean necesarias las capacidades completas de temas. Styles no se debe intercambiar de forma condicional, a menos que todo el estilo sea fundamentalmente diferente. Debes preferir acceder a tokens dinámicos dentro de una definición visual en lugar de cambiar entre objetos de estilo distintos.