Aplicación de temas con estilos

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

  1. Sistema de diseño completamente personalizado que no usa 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. Cómo usar Material Design
    • Recomendación: Espera a que se adopte Material para integrar con Styles. Usa estilos en tus propios componentes siempre que sea posible.

La capa de diseño

En el modelo tradicional de Compose, la personalización suele depender en gran medida de la anulación de tokens globales (colores y tipografía) proporcionados por MaterialTheme, o bien de la anulación y el ajuste de propiedades de un elemento componible del sistema de diseño siempre que sea posible. A veces, hay propiedades dentro de la capa Material que no se exponen a través de los subsistemas o parámetros, sino que son valores predeterminados codificados 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)
Atomic Styles Estilo que realiza exactamente un cambio de propiedad val largeSizeAtomic = Style { size(100.dp, 40.dp) }
Estilos de componentes Configuraciones específicas del componente Un botón con fondo principal y padding de 16 dp. val buttonStyle = Style { contentPadding(16.dp) shape(RoundedCornerShape(8.dp)) background(Color.Blue) }
Componentes Es el elemento de la IU funcional que consume un diseño. Button(style = buttonStyle) { ... }
Diagrama que muestra la aplicación de temas con diseños y la introducción de la nueva capa
Figura 1: Ejemplo de un componente y cómo accede a los estilos de un tema.

Comparación entre los estilos atómicos y los monolíticos

Con la API de Styles, puedes desglosar un diseño en diseños atómicos separados. En lugar de definir estilos complejos y específicos de los componentes, como baseButtonStyle, también puedes crear pequeños estilos de utilidad 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 funciones potentes 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ómica):

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

Consideraciones: Recibiste una guía de marca extensa que no se basa en Material Design y no planeas usar Material Design.

Estrategia: Implementa un sistema de diseño completamente 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 del sistema de diseño. Omitiste MaterialTheme por completo para las definiciones visuales y ya creaste tu propio tema personalizado. Compilas un CompanyTheme que actúa como contenedor para tus objetos Styles.

  • Cómo funciona: Crea un objeto CompanyTheme que contenga objetos Style para cada componente de tu sistema. Tus componentes (ya sean contenedores 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 diseño: Los diseños son la definición principal de tu sistema de diseño. Los tokens son variables con nombre que se incorporan a 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 estás creando 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 básicos desde cualquier lugar de tu proyecto.

  1. Crea una clase Styles que almacene los distintos diseños de tu aplicación y crea 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 del tema global, existen estrategias alternativas para incorporar Styles en tus apps. Puedes aprovechar Styles intercalado para sitios de llamadas específicos o usar definiciones estáticas cuando no se necesitan capacidades de temas completas. Styles no se debe intercambiar de forma condicional, a menos que todo el diseño sea fundamentalmente diferente. Te recomendamos que accedas a los tokens dinámicos dentro de una definición visual en lugar de cambiar entre objetos de estilo distintos.