wandres.dev
MATERIAL 3 · theming y color dinámico

Un tema propio: tu marca sin pelearte con Material

Material 3 admite exactamente tres pilares y tu sistema de diseño casi con seguridad tiene más: espaciados, elevaciones nombradas, colores semánticos de negocio, degradados, duraciones de animación. Esta lección construye la capa que falta sin romper la que existe: cómo derivar un esquema completo desde un color semilla en lugar de escribir treinta valores a mano, cómo extender el tema con variables de composición propias y un objeto de acceso homogéneo, por qué conviene encapsular la biblioteca detrás de una puerta única de módulo, y cuál es el criterio honesto para decidir entre tematizar Material o construir un sistema propio sobre la capa de fundamentos, con el inventario completo de lo que se pierde al hacerlo.

⏱ 20 min

La conversación se repite en todos los equipos. Llega la guía de estilo, alguien la abre por la página de los colores y descubre que hay once, no treinta; que los espaciados tienen nombre y Material no los contempla; que existe un verde de “operación confirmada” que no encaja en ningún rol; y que la tipografía de marca tiene dos pesos, no cinco tamaños en cinco familias. A partir de ahí se abren dos caminos y ambos tienen un final conocido. Uno consiste en forzar la guía dentro de los tres pilares hasta que ninguno de los dos lados se reconoce. El otro consiste en declarar que Material no sirve y empezar de cero, para reimplementar mal, dieciocho meses después, las capas de estado, los tamaños mínimos de pulsación y la semántica de accesibilidad que venían resueltas. Existe un tercer camino, menos épico y mucho más productivo: aceptar Material como capa de mecánica y construir encima la capa de vocabulario que te falta.

🎯 Al terminar esta lección sabrás
  • Generar un esquema de color completo y coherente a partir de los colores de marca.
  • Extender el tema con dimensiones propias sin romper el modelo de propagación.
  • Encapsular la biblioteca tras una puerta única para poder cambiarla algún día.
  • Aplicar un criterio explícito para decidir entre tematizar Material o construir un sistema propio.

Empieza por los tokens, nunca por los componentes

El error inicial más frecuente es abrir el fichero del botón. Un sistema de diseño no empieza en los componentes: empieza en los valores nombrados de los que todos los componentes beben. Si los tokens están bien, la personalización de los componentes se vuelve casi innecesaria; si están mal, ninguna cantidad de parámetros la salvará.

Para el color, la escritura manual de los treinta y tantos roles es un camino directo a la incoherencia: nadie mantiene a mano las relaciones de luminosidad entre un contenedor y su contenido, ni las replica correctamente en el esquema oscuro. Lo razonable es derivar el esquema desde dos o tres colores semilla con las mismas herramientas que usa el color dinámico, revisar el resultado y ajustar solo los roles que de verdad discrepen de la marca.

// Los roles derivados se aceptan; solo se sobrescribe lo innegociable.
private val ClaroMarca = lightColorScheme(
    primary = MarcaAzul600,
    onPrimary = Color.White,
    primaryContainer = MarcaAzul100,
    onPrimaryContainer = MarcaAzul900,
    error = MarcaRojo600,
)

private val OscuroMarca = darkColorScheme(
    primary = MarcaAzul200,
    onPrimary = MarcaAzul900,
    primaryContainer = MarcaAzul700,
    onPrimaryContainer = MarcaAzul050,
    error = MarcaRojo200,
)

La tipografía admite el mismo razonamiento. Sustituir la familia en los quince estilos es mecánico; lo que exige criterio es respetar la jerarquía existente en lugar de inventar una nueva. Si tu guía tiene menos niveles que Material, mapea varios estilos al mismo valor antes que dejar huecos: los componentes de la biblioteca van a pedir estilos que tú no habías previsto, y más vale que encuentren algo sensato.

Y hay un detalle tipográfico que se pasa por alto y produce interfaces rotas en producción: los tamaños de texto deben expresarse en unidades escalables para que respeten el ajuste de tamaño de fuente del sistema. Fijarlos en unidades de densidad para que “no se descoloque el diseño” es exactamente el fallo que deja sin usar la aplicación a quien necesita el texto grande. Si tu disposición se rompe con el texto al doble de tamaño, el problema está en la disposición, no en la preferencia del usuario.

Lo que Material no tiene y cómo añadirlo

Los espaciados, las duraciones, los colores semánticos de negocio y las elevaciones nombradas no caben en los tres pilares, y la respuesta correcta no es meterlos a la fuerza sino replicar el mecanismo. Se declara una variable de composición propia, se instala en el mismo composable que instala el tema y se expone a través de un objeto de acceso con la misma forma que el de la biblioteca, para que el punto de uso no distinga entre lo que viene de Material y lo que viene de casa.

@Immutable
data class Espaciado(val xs: Dp = 4.dp, val s: Dp = 8.dp, val m: Dp = 16.dp, val l: Dp = 24.dp)

val LocalEspaciado = staticCompositionLocalOf { Espaciado() }

object TemaApp {
    val espaciado: Espaciado
        @Composable @ReadOnlyComposable get() = LocalEspaciado.current
}

@Composable
fun TemaApp(oscuro: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalEspaciado provides Espaciado()) {
        MaterialTheme(
            colorScheme = if (oscuro) OscuroMarca else ClaroMarca,
            typography = TipografiaMarca,
            shapes = FormasMarca,
            content = content,
        )
    }
}
📏

Dimensiones

Espaciados y tamaños nombrados. Cámbialos por densidad o por dispositivo sin tocar una sola pantalla.

🚦

Color semántico

Éxito, aviso, informativo. Roles de negocio con su acompañante legible, armonizados hacia el esquema.

⏱️

Movimiento

Duraciones y curvas nombradas. Sin ellas, cada animación negocia su propio tiempo y nada va acompasado.

💡
Elige bien entre variable estatica y variable observada

La variante estática no rastrea lecturas: cuando cambia, recompone el subárbol entero. Es la elección correcta para valores que apenas cambian, como los espaciados. La variante observada rastrea cada lectura y es más cara de instalar, pero recompone solo a quien la leyó; resérvala para lo que cambia durante la sesión. Instalar un espaciado con la variante observada es pagar un coste permanente por una flexibilidad que nunca usarás.

La puerta única

Un sistema de diseño que vive esparcido por la aplicación no es un sistema, es una convención. La disciplina que lo convierte en infraestructura es simple de enunciar y dura de sostener: la biblioteca de Material solo se importa dentro del módulo de diseño, y el resto de la aplicación consume exclusivamente componentes propios que envuelven a los de la biblioteca con la API que tu producto necesita.

El beneficio inmediato es que las decisiones se toman una vez. El botón principal tiene el mismo relleno, el mismo estado de carga y el mismo comportamiento ante pulsaciones repetidas en las noventa pantallas, porque hay un solo botón. El beneficio diferido es mayor: cuando la biblioteca publique una revisión que renombre parámetros o cambie el aspecto de un componente, el impacto se concentra en un módulo en lugar de repartirse por todo el repositorio. Esa frontera se puede vigilar con una regla de análisis estático que falle la compilación ante cualquier importación de la biblioteca fuera del módulo autorizado.

// La aplicacion consume esto; nadie fuera del modulo de diseno
// conoce el nombre del componente de la biblioteca.
@Composable
fun BotonPrimario(
    texto: String,
    alPulsar: () -> Unit,
    modifier: Modifier = Modifier,
    cargando: Boolean = false,
    habilitado: Boolean = true,
) {
    Button(onClick = alPulsar, modifier = modifier, enabled = habilitado && !cargando) {
        if (cargando) IndicadorCarga() else Text(texto)
    }
}

La objeción habitual es que esa capa añade indirección sin aportar nada. Se responde sola en cuanto llega el primer requisito transversal: un registro de analítica en cada pulsación, una protección contra la doble pulsación, un estado de carga uniforme, un cambio de tamaño mínimo para tabletas. Con puerta única, cada uno de esos requisitos es una modificación en un fichero; sin ella, es una migración.

Cuándo dejar de usar Material

Hay productos cuyo lenguaje visual contradice a Material de raíz. Para ellos existe la opción legítima de construir sobre la capa de fundamentos, que aporta disposición, gestos, dibujo y semántica sin imponer ninguna estética. Pero conviene hacer el inventario completo de lo que se abandona antes de firmar.

flowchart TD
A[Tu guia de estilo contradice a Material] --> B{Contradice la estetica o la mecanica}
B -->|Solo la estetica| C[Tematiza tokens y envuelve componentes]
B -->|Tambien la mecanica| D{Tienes equipo dedicado y sostenido}
D -->|No| C
D -->|Si| E[Sistema propio sobre la capa de fundamentos]
E --> F[Reimplementa capas de estado y tamanos minimos]
E --> G[Reimplementa semantica y foco de teclado]
E --> H[Reimplementa insets y comportamiento adaptativo]
style C fill:#a6e3a1,color:#11111b
style F fill:#f9e2af,color:#11111b
style G fill:#f9e2af,color:#11111b
style H fill:#f9e2af,color:#11111b
⚠️
Lo que se pierde no es el aspecto, es el comportamiento

Los componentes de Material traen resueltos el tamaño mínimo de área táctil, las capas de estado para pulsación y foco, el papel semántico para los lectores de pantalla, el recorrido de teclado, la gestión de insets en barras y hojas, y las adaptaciones de tamaño de ventana. Nada de eso se ve en una captura de pantalla, y todo eso lo hereda tu equipo el día que decide construir de cero.

Un sistema de diseno es una teoria sobre tu producto, no un catalogo

La pregunta “Material o sistema propio” está mal formulada, y por eso genera discusiones interminables que se resuelven por gusto. La formulación útil separa dos capas que la mayoría de los equipos confunde: existe una mecánica de interacción —cómo se comporta algo pulsable, qué le pasa cuando recibe el foco, cuánto mide el área que responde al dedo, cómo se anuncia a un lector de pantalla, qué ocurre cuando aparece el teclado— y existe un vocabulario estético —qué colores, qué tipos, qué radios, qué ritmos—. Material es una implementación excelente de la primera y una propuesta concreta de la segunda, y son separables: los tres pilares existen precisamente para que puedas reemplazar el vocabulario conservando la mecánica. Casi todos los equipos que dicen “Material no encaja con nuestra marca” están hablando del vocabulario, y para eso la respuesta es tematizar y envolver, no reconstruir. Reconstruir solo se justifica cuando la contradicción es mecánica: un producto cuyo modelo de interacción es genuinamente distinto —una herramienta de dibujo, un instrumento musical, un juego—. Y hay una asimetría que conviene tener presente al elegir, porque no es reversible en la práctica: el vocabulario se puede cambiar en una tarde si vive en tokens, mientras que la mecánica tarda años en alcanzar la calidad que la biblioteca ya tiene, y su deuda no se manifiesta como fealdad sino como usuarios que no pueden usar tu aplicación y que nunca te lo van a contar. Escribir tu propio sistema de diseño no es un acto de identidad: es asumir el mantenimiento perpetuo de una biblioteca de interfaz, y esa decisión debería tomarse con la misma seriedad con la que se decide escribir tu propia base de datos.

⚔️ Construye la capa que te falta
  1. Inventaría los valores nombrados de tu guía de estilo y clasifícalos en tres columnas: cabe en un pilar de Material, necesita una variable propia, es un componente disfrazado de token.
  2. Deriva tu esquema completo desde los colores semilla y compáralo con el que habías escrito a mano; anota cada discrepancia y decide cuál de las dos versiones estaba mal.
  3. Implementa una dimensión propia con su variable de composición y su objeto de acceso, y justifica la variante que elegiste.
  4. Cuenta cuántos ficheros de tu aplicación importan la biblioteca de Material directamente y estima el coste de reducirlos a uno.
  5. Elige el componente que más habéis personalizado y decide, argumentando, si envolverlo, copiar su código al módulo de diseño o aceptar el comportamiento de la biblioteca.