Compose proporciona muchos modificadores para comportamientos comunes listos para usar, pero también puedes crear tus propios modificadores personalizados.
Los modificadores tienen varias partes:
- Una fábrica de modificadores
- Esta es una función de extensión en
Modifier
, que proporciona una API idiomática para tu modificador y permite que los modificadores se encadenan juntos fácilmente. La fábrica del modificador produce los elementos modificadores que usa Compose para modificar tu IU.
- Esta es una función de extensión en
- Un elemento modificador.
- Aquí es donde puedes implementar el comportamiento de tu modificador.
Hay varias formas de implementar un modificador personalizado según la funcionalidad necesaria. A menudo, la manera más fácil de implementar un modificador personalizado es implementar una fábrica de modificadores personalizados que combine otras fábricas de modificador ya definidas. Si necesitas un comportamiento más personalizado, implementa el elemento modificador con las APIs de Modifier.Node
, que son de nivel inferior, pero proporcionan más flexibilidad.
Cómo encadenar modificadores existentes
A menudo, es posible crear modificadores personalizados con solo usar los modificadores existentes. Por ejemplo, Modifier.clip()
se implementa con el modificador graphicsLayer
. En esta estrategia, se usan elementos modificadores existentes y tú proporcionas tu propia fábrica de modificadores personalizados.
Antes de implementar tu propio modificador personalizado, comprueba si puedes usar la misma estrategia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
O, si descubres que se repite el mismo grupo de modificadores a menudo, puedes unirlos en tu propio modificador:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Cómo crear un modificador personalizado con una fábrica de modificadores componible
También puedes crear un modificador personalizado con una función de componibilidad para pasar valores a un modificador existente. Esto se conoce como una fábrica de modificadores componibles.
El uso de una fábrica de modificadores componible para crear un modificador también permite usar APIs de Compose de nivel superior, como animate*AsState
y otras APIs de animación respaldadas por estado de Compose. Por ejemplo, en el siguiente fragmento, se muestra un modificador que anima un cambio alfa cuando está habilitado o inhabilitado:
@Composable fun Modifier.fade(enable: Boolean): Modifier { val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f) return this then Modifier.graphicsLayer { this.alpha = alpha } }
Si tu modificador personalizado es un método conveniente para proporcionar valores predeterminados de un CompositionLocal
, la manera más fácil de implementarlo es usar una fábrica de modificador componible:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Este enfoque tiene algunas advertencias que se detallan a continuación.
Los valores CompositionLocal
se resuelven en el sitio de llamada de la fábrica de modificadores.
Cuando se crea un modificador personalizado con una fábrica de modificadores componible, los elementos locales de composición toman el valor del árbol de composición en el que se crean y no se usan. Esto puede generar resultados inesperados. Por ejemplo, toma el ejemplo del modificador local de composición anterior, implementado de forma un poco diferente con una función de componibilidad:
@Composable fun Modifier.myBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) } @Composable fun MyScreen() { CompositionLocalProvider(LocalContentColor provides Color.Green) { // Background modifier created with green background val backgroundModifier = Modifier.myBackground() // LocalContentColor updated to red CompositionLocalProvider(LocalContentColor provides Color.Red) { // Box will have green background, not red as expected. Box(modifier = backgroundModifier) } } }
Si no es así como esperas que funcione el modificador, usa un Modifier.Node
personalizado, ya que los parámetros de composición local se resolverán correctamente en el sitio de uso y se podrán elevar de forma segura.
Nunca se omiten los modificadores de funciones de componibilidad.
Los modificadores de fábrica componibles nunca se omiten porque las funciones de componibilidad que tienen valores de retorno no se pueden omitir. Esto significa que se llamará a tu función de modificador en cada recomposición, lo que puede ser costoso si se recompone con frecuencia.
Se debe llamar a los modificadores de funciones de componibilidad dentro de una función de componibilidad.
Al igual que todas las funciones de componibilidad, se debe llamar a un modificador de fábrica componible desde la composición. Este límite es el que se puede elevar a un modificador, ya que nunca se puede elevar fuera de la composición. En comparación, las fábricas de modificadores no componibles se pueden elevar de las funciones de componibilidad para facilitar la reutilización y mejorar el rendimiento:
val extractedModifier = Modifier.background(Color.Red) // Hoisted to save allocations @Composable fun Modifier.composableModifier(): Modifier { val color = LocalContentColor.current.copy(alpha = 0.5f) return this then Modifier.background(color) } @Composable fun MyComposable() { val composedModifier = Modifier.composableModifier() // Cannot be extracted any higher }
Implementa el comportamiento de los modificadores personalizados con Modifier.Node
Modifier.Node
es una API de nivel inferior para crear modificadores en Compose. Es la misma API en la que Compose implementa sus propios modificadores y es la forma más eficaz de crear modificadores personalizados.
Implementa un modificador personalizado con Modifier.Node
Hay tres partes para implementar un modificador personalizado con Modifier.Node:
- Una implementación de
Modifier.Node
que contiene la lógica y el estado del modificador - Un
ModifierNodeElement
que crea y actualiza instancias de nodos modificadores - Una fábrica de modificadores opcional, como se detalló anteriormente.
Las clases ModifierNodeElement
no tienen estado y se asignan instancias nuevas a cada recomposición, mientras que las clases Modifier.Node
pueden tener estado y sobrevivirán a varias recomposiciones, e incluso se pueden volver a usar.
En la siguiente sección, se describe cada parte y se muestra un ejemplo de cómo crear un modificador personalizado para dibujar un círculo.
Modifier.Node
La implementación de Modifier.Node
(en este ejemplo, CircleNode
) implementa la funcionalidad de tu modificador personalizado.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
En este ejemplo, dibuja el círculo con el color que se pasó a la función modificadora.
Un nodo implementa Modifier.Node
, así como cero o más tipos de nodos. Existen diferentes tipos de nodos según la funcionalidad que requiera tu modificador. En el ejemplo anterior, se debe poder dibujar, por lo que implementa DrawModifierNode
, que le permite anular el método de dibujo.
Los tipos disponibles son los siguientes:
Nodo |
Uso |
Vínculo de muestra |
Es un |
||
Es un |
||
Implementar esta interfaz permite que tu |
||
Un |
||
Un objeto |
||
Un |
||
Un |
||
Un objeto |
||
Los elementos |
||
Un Esto puede ser útil para componer varias implementaciones de nodo en una. |
||
Permite que las clases |
Los nodos se invalidan de forma automática cuando se llama a Update en el elemento correspondiente. Como nuestro ejemplo es una DrawModifierNode
, cada vez que se llama a una actualización en el elemento, el nodo activa un nuevo dibujo y su color se actualiza correctamente. Puedes inhabilitar la invalidación automática tal como se detalla a continuación.
ModifierNodeElement
Una ModifierNodeElement
es una clase inmutable que contiene los datos para crear o actualizar tu modificador personalizado:
// ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } }
Las implementaciones de ModifierNodeElement
deben anular los siguientes métodos:
create
: Esta es la función con la que se crea una instancia de tu nodo modificador. Se llama a este método para crear el nodo cuando se aplica el modificador por primera vez. Por lo general, esto equivale a construir el nodo y configurarlo con los parámetros que se pasaron a la fábrica de modificadores.update
: Se llama a esta función cada vez que se proporciona este modificador en el mismo lugar donde ya existe este nodo, pero cambia una propiedad. Esto se determina mediante el métodoequals
de la clase. El nodo modificador que se creó antes se envía como parámetro a la llamada aupdate
. En este punto, debes actualizar las propiedades de los nodos para que se correspondan con los parámetros actualizados. La capacidad de que los nodos se reutilicen de esta manera es clave para las mejoras de rendimiento queModifier.Node
genera. Por lo tanto, debes actualizar el nodo existente en lugar de crear uno nuevo en el métodoupdate
. En nuestro ejemplo de círculo, se actualiza el color del nodo.
Además, las implementaciones de ModifierNodeElement
también deben implementar equals
y hashCode
. Solo se llamará a update
si una comparación de igual con el elemento anterior muestra un valor falso.
En el ejemplo anterior, se usa una clase de datos para lograrlo. Estos métodos se usan para verificar si un nodo debe actualizarse o no. Si tu elemento tiene propiedades que no contribuyen a la actualización de un nodo, o bien si deseas evitar las clases de datos por motivos de compatibilidad binaria, puedes implementar equals
y hashCode
de forma manual; p.ej., el elemento modificador de padding.
Fábrica de modificadores
Esta es la plataforma de la API pública de tu modificador. La mayoría de las implementaciones simplemente crean el elemento modificador y lo agregan a la cadena del modificador:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Ejemplo completo
Estas tres partes se unen para crear el modificador personalizado a fin de dibujar un círculo con las APIs de Modifier.Node
:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color) // ModifierNodeElement private data class CircleElement(val color: Color) : ModifierNodeElement<CircleNode>() { override fun create() = CircleNode(color) override fun update(node: CircleNode) { node.color = color } } // Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Situaciones comunes que usan Modifier.Node
A continuación, se mencionan algunas situaciones comunes que podrías encontrar cuando creas modificadores personalizados con Modifier.Node
.
Cero parámetros
Si tu modificador no tiene parámetros, no necesita actualizarse y, además, no necesita ser una clase de datos. A continuación, se muestra una implementación de ejemplo de un modificador que aplica una cantidad fija de padding a un elemento componible:
fun Modifier.fixedPadding() = this then FixedPaddingElement data object FixedPaddingElement : ModifierNodeElement<FixedPaddingNode>() { override fun create() = FixedPaddingNode() override fun update(node: FixedPaddingNode) {} } class FixedPaddingNode : LayoutModifierNode, Modifier.Node() { private val PADDING = 16.dp override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val paddingPx = PADDING.roundToPx() val horizontal = paddingPx * 2 val vertical = paddingPx * 2 val placeable = measurable.measure(constraints.offset(-horizontal, -vertical)) val width = constraints.constrainWidth(placeable.width + horizontal) val height = constraints.constrainHeight(placeable.height + vertical) return layout(width, height) { placeable.place(paddingPx, paddingPx) } } }
Cómo hacer referencia a la composición local
Los modificadores Modifier.Node
no observan automáticamente los cambios en los objetos de estado de Compose, como CompositionLocal
. La ventaja de que los modificadores Modifier.Node
tienen sobre los modificadores que se acaban de crear con una fábrica componible es que pueden leer el valor del elemento local de composición desde donde se usa el modificador en tu árbol de IU, no desde donde se asigna, con currentValueOf
.
Sin embargo, las instancias de nodos modificadores no observan los cambios de estado automáticamente. Para reaccionar automáticamente a un cambio local de composición, puedes leer su valor actual dentro de un alcance:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
yIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
En este ejemplo, se observa el valor de LocalContentColor
para dibujar un fondo en función de su color. Como ContentDrawScope
observa cambios en la instantánea, se vuelve a dibujar automáticamente cuando cambia el valor de LocalContentColor
:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Para reaccionar a los cambios de estado fuera de un alcance y actualizar automáticamente tu modificador, usa un ObserverModifierNode
.
Por ejemplo, Modifier.scrollable
usa esta técnica para observar cambios en LocalDensity
. A continuación, se muestra un ejemplo simplificado:
class ScrollableNode : Modifier.Node(), ObserverModifierNode, CompositionLocalConsumerModifierNode { // Place holder fling behavior, we'll initialize it when the density is available. val defaultFlingBehavior = DefaultFlingBehavior(splineBasedDecay(UnityDensity)) override fun onAttach() { updateDefaultFlingBehavior() observeReads { currentValueOf(LocalDensity) } // monitor change in Density } override fun onObservedReadsChanged() { // if density changes, update the default fling behavior. updateDefaultFlingBehavior() } private fun updateDefaultFlingBehavior() { val density = currentValueOf(LocalDensity) defaultFlingBehavior.flingDecay = splineBasedDecay(density) } }
Modificador de animación
Las implementaciones de Modifier.Node
tienen acceso a un objeto coroutineScope
. Esto permite el uso de las APIs de Compose Animatable. Por ejemplo, este fragmento modifica la CircleNode
anterior para atenuar de entrada y salida de forma repetida:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private val alpha = Animatable(1f) override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Comparte el estado entre modificadores mediante la delegación
Los modificadores Modifier.Node
pueden delegar a otros nodos. Hay muchos casos de uso para esto, como la extracción de implementaciones comunes a través de diferentes modificadores, pero también se puede usar para compartir un estado común entre modificadores.
Por ejemplo, una implementación básica de un nodo modificador en el que se puede hacer clic que comparte datos de interacción:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Cómo inhabilitar la invalidación automática de nodos
Los nodos Modifier.Node
se invalidan automáticamente cuando se actualizan sus llamadas a ModifierNodeElement
correspondientes. A veces, en un modificador más complejo, es posible que quieras inhabilitar este comportamiento para tener un control más detallado sobre cuándo tu modificador invalida fases.
Esto puede ser muy útil si tu modificador personalizado modifica el diseño y el dibujo. Inhabilitar la invalidación automática te permite invalidar el dibujo solo cuando las propiedades relacionadas, como color
, cambian y no invalidan el diseño.
Esto puede mejorar el rendimiento del modificador.
A continuación, se muestra un ejemplo hipotético de esto con un modificador que tiene una lambda color
, size
y onClick
como propiedades. Este modificador solo invalida lo que es necesario y omite cualquier invalidación que no sea:
class SampleInvalidatingNode( var color: Color, var size: IntSize, var onClick: () -> Unit ) : DelegatingNode(), LayoutModifierNode, DrawModifierNode { override val shouldAutoInvalidate: Boolean get() = false private val clickableNode = delegate( ClickablePointerInputNode(onClick) ) fun update(color: Color, size: IntSize, onClick: () -> Unit) { if (this.color != color) { this.color = color // Only invalidate draw when color changes invalidateDraw() } if (this.size != size) { this.size = size // Only invalidate layout when size changes invalidateMeasurement() } // If only onClick changes, we don't need to invalidate anything clickableNode.update(onClick) } override fun ContentDrawScope.draw() { drawRect(color) } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints ): MeasureResult { val size = constraints.constrain(size) val placeable = measurable.measure(constraints) return layout(size.width, size.height) { placeable.place(0, 0) } } }