Modificadores: la cadena que envuelve al nodo
El sistema de decoración de Compose desmontado: la cadena inmutable y ordenada de Modifier, por qué el orden cambia el resultado en lugar de acumularse, cómo se reparten los elementos entre las fases de layout y dibujo, y cómo escribir un modificador propio con la API de nodos.
El parámetro más repetido de todo Compose es también el peor entendido. Un Modifier no es un saco de propiedades que se aplican a un elemento, ni un equivalente de los atributos de una etiqueta XML: es una lista enlazada, inmutable y estrictamente ordenada, donde cada eslabón envuelve al siguiente y decide qué le llega y qué sale de él. Por eso rellenar y luego pintar el fondo no produce lo mismo que pintar el fondo y luego rellenar, y por eso la pregunta correcta ante una cadena no es qué hace cada modificador, sino en qué orden se atraviesan y en qué fase actúa cada uno.
- Describir la cadena de
Modifiercomo estructura inmutable, ordenada y encadenada. - Explicar por qué el orden altera el resultado en lugar de acumularse.
- Situar cada tipo de elemento en su fase: layout, dibujo, entrada o semántica.
- Escribir un modificador propio con la API de nodos y saber por qué se prefiere.
Una lista enlazada e inmutable
Modifier es una interfaz con un objeto vacío como identidad y una operación de concatenación. Cada llamada encadenada no muta nada: devuelve una cadena nueva formada por la anterior más un eslabón. El resultado es una estructura persistente, comparable y compartible, que se puede guardar en una constante y reutilizar en muchos sitios sin riesgo.
val destacado = Modifier
.padding(16.dp)
.background(Color.Yellow)
.clip(RoundedCornerShape(8.dp))
// equivale a componer eslabones con la operacion then
val equivalente = Modifier
.then(PaddingElement(16.dp))
.then(BackgroundElement(Color.Yellow))
Todas las funciones que ves como Modifier.algo son en realidad funciones de extensión sobre Modifier que añaden un elemento al final. Que sean extensiones tiene una consecuencia práctica agradable: cualquiera puede añadir modificadores propios sin tocar la biblioteca, y el sistema queda abierto sin necesidad de herencia.
La cadena viaja hasta el elemento como un parámetro más, y el convenio de la biblioteca es firme: toda función componible pública que emita interfaz debe aceptar un parámetro modifier con valor por defecto vacío, colocado como primer parámetro opcional, y aplicarlo al elemento más externo que emita. Romper ese convenio convierte tu componente en una caja opaca que nadie puede colocar, dimensionar ni decorar desde fuera.
Por qué el orden cambia el resultado
Aquí está el punto que separa a quien copia cadenas de quien las diseña. Los modificadores no se acumulan en un montón de atributos que después se resuelven juntos: se atraviesan. El primero de la cadena es el más externo y el último es el más interno, de manera que las restricciones de medida descienden desde el primero hacia el último, y los tamaños ascienden en sentido contrario.
// El relleno queda FUERA del fondo: aparece un margen sin pintar
Text("Hola", Modifier.padding(16.dp).background(Color.Yellow))
// El relleno queda DENTRO del fondo: el amarillo cubre tambien el relleno
Text("Hola", Modifier.background(Color.Yellow).padding(16.dp))
La explicación mecánica es directa. En el primer caso, el eslabón de relleno recibe las restricciones del padre, reserva dieciséis puntos por cada lado y pasa unas restricciones más pequeñas al eslabón de fondo; el fondo pinta el área que le corresponde, que ya no incluye el relleno. En el segundo, el fondo es el eslabón externo y recibe las restricciones completas, así que pinta todo el área disponible, y el relleno solo afecta a lo que queda por dentro.
flowchart TD P[Padre] -->|restricciones| M1[Primer eslabon: el mas externo] M1 -->|restricciones reducidas| M2[Segundo eslabon] M2 -->|restricciones reducidas| N[Nodo del contenido] N -->|tamano medido| M2 M2 -->|tamano| M1 M1 -->|tamano final| P style M1 fill:#f9e2af,color:#11111b style N fill:#a6e3a1,color:#11111b
El mismo razonamiento resuelve las dudas clásicas sin necesidad de memorizar casos. Un modificador de pulsación colocado antes del relleno hace que el área sensible incluya el relleno; colocado después, solo cubre el contenido. Un recorte colocado antes de una sombra recorta la sombra; colocado después, no. Y encadenar dos veces el mismo tipo de modificador no es un error ni se sobrescribe: se aplican los dos, uno dentro del otro, que es exactamente lo que permite construir bordes concéntricos con rellenos intercalados.
Lee la cadena de izquierda a derecha como si fueras entrando en cajas anidadas: cada eslabón dibuja o mide alrededor de todo lo que viene después. Si algo aparece con más área de la que esperabas, prueba a moverlo hacia la derecha; si aparece con menos, muévelo hacia la izquierda.
Cada eslabón actúa en su fase
Un modificador no es una cosa única con muchas variantes: es una familia de tipos distintos que el nodo consulta en momentos distintos del fotograma. Distinguirlos es lo que permite razonar sobre el coste de una cadena.
De layout
Alteran las restricciones que bajan o el tamaño que sube. Relleno, tamaño, desplazamiento, alineación.
De dibujo
Emiten órdenes gráficas antes o después del contenido. Fondo, borde, sombra, dibujo personalizado.
De entrada
Registran regiones sensibles al puntero o al foco. Pulsación, arrastre, gestos, navegación por teclado.
De semántica
No producen nada visible: describen el elemento al árbol de accesibilidad y a las pruebas.
Esa clasificación tiene una lectura de rendimiento inmediata. Un modificador de layout que lee un valor cambiante fuerza medida y dibujo; uno de dibujo que lee ese mismo valor solo fuerza dibujo. Por eso las variantes que reciben una lambda, como el desplazamiento calculado o el dibujo diferido, existen en paralelo a las que reciben un valor directo: no son azúcar sintáctico, son puntos de entrada a fases más tardías.
Escribir el tuyo
La forma más simple de crear un modificador propio es componer los que ya existen dentro de una función de extensión. No requiere nada especial y resuelve la mayoría de los casos reales, que son de reutilización, no de comportamiento nuevo.
fun Modifier.tarjeta(color: Color): Modifier = this
.clip(RoundedCornerShape(12.dp))
.background(color)
.padding(16.dp)
Cuando de verdad necesitas comportamiento nuevo —medir de otra manera, dibujar algo que no existe, interpretar el puntero— hay dos caminos. El rápido son las lambdas de bajo nivel que la biblioteca expone: Modifier.layout recibe un medible y unas restricciones y devuelve el resultado de la colocación, y Modifier.drawBehind recibe un ámbito de dibujo. Sirven perfectamente para casos puntuales.
// Alinear el texto por su linea base sin envolver en otro contenedor
fun Modifier.primeraLineaBase(desde: Dp) = layout { medible, restricciones ->
val colocable = medible.measure(restricciones)
val base = colocable[FirstBaseline]
val alturaTotal = colocable.height + desde.roundToPx() - base
layout(colocable.width, alturaTotal) {
colocable.placeRelative(0, desde.roundToPx() - base)
}
}
El camino serio, y el recomendado desde que la biblioteca migró internamente a él, es la API de nodos. Un modificador se parte en dos piezas: un elemento, que es un dato ligero y comparable con los parámetros, y un nodo, que es un objeto de larga vida donde reside el comportamiento y el estado interno. Cuando los parámetros cambian, el runtime no destruye el nodo: lo actualiza.
private class SubrayadoNode(var color: Color) : Modifier.Node(), DrawModifierNode {
override fun ContentDrawScope.draw() {
drawContent()
drawLine(color, Offset(0f, size.height), Offset(size.width, size.height))
}
}
private data class SubrayadoElement(val color: Color) : ModifierNodeElement<SubrayadoNode>() {
override fun create() = SubrayadoNode(color)
override fun update(node: SubrayadoNode) { node.color = color }
}
fun Modifier.subrayado(color: Color): Modifier = this then SubrayadoElement(color)
La ventaja no es estética. El antiguo mecanismo basado en composición interna creaba una composición por cada uso del modificador, lo que impedía comparar cadenas por igualdad y generaba asignaciones y recomposiciones en sitios donde no aportaban nada. Con la API de nodos, el elemento es un tipo de datos comparable, el nodo persiste entre recomposiciones y las actualizaciones de parámetros no pasan por la fase de composición en absoluto. En cadenas que se aplican a miles de elementos de una lista, la diferencia es medible.
Lo que hace especial al sistema de modificadores no es su ergonomía, sino que resuelve un viejo dilema de diseño de interfaces sin pagar su precio habitual. Cada vez que quieres añadir relleno, fondo, recorte o detección de gestos a un elemento, estás pidiendo un comportamiento de envoltura, y la solución tradicional era literalmente envolver: un contenedor por cada preocupación, jerarquías que crecían en profundidad hasta hacerse caras de medir y difíciles de leer. Compose se queda con la semántica de anidamiento y tira la estructura: la cadena de modificadores es una jerarquía de envolturas, con su orden y su anidamiento, pero se aplana en una lista enlazada dentro de un solo nodo del árbol. De ahí salen a la vez las tres propiedades que parecían incompatibles. Es composicional, porque cada eslabón solo conoce a su vecino y puedes insertar comportamiento en cualquier punto sin tocar el resto. Es barata, porque no multiplica nodos de layout y el árbol conserva su profundidad real. Y es honesta con el orden, porque no puede fingir que las decoraciones conmutan cuando en realidad no lo hacen: la confusión de los principiantes con el orden de relleno y fondo no es un fallo de diseño, es el sistema negándose a ocultar que envolver por fuera y envolver por dentro son cosas distintas. Entender la cadena así cambia la manera de escribir componentes: dejas de preguntarte qué contenedor necesitas para conseguir un efecto y empiezas a preguntarte en qué punto de la cadena hay que intervenir, lo que casi siempre resulta en menos elementos, menos parámetros de configuración y APIs que otros pueden extender sin pedirte permiso.
- Coge un elemento con relleno, fondo, borde y detección de pulsación, y prueba las seis permutaciones posibles anotando qué cambia en cada una.
- Añade dos rellenos con un borde intercalado y explica por qué el resultado son dos marcos concéntricos.
- Escribe un modificador de extensión que agrupe la decoración de tu tarjeta y aplícalo en tres pantallas distintas.
- Implementa un modificador de dibujo con la API de nodos que pinte una franja diagonal, y verifica que al cambiar el color no se recompone nada.
- Comprueba con el inspector qué ocurre cuando olvidas aceptar un parámetro
modifieren un componente público y alguien intenta dimensionarlo desde fuera.