Compose proporciona muchos modificadores para comportamientos comunes de forma predeterminada, pero también puedes crear tus propios modificadores personalizados.
Los modificadores tienen varias partes:
- Una fábrica de modificadores
- Es una función de extensión en
Modifier, que proporciona una API idiomática para tu modificador y permite que los modificadores se encadenen. La fábrica de modificadores produce los elementos modificadores que usa Compose para modificar tu IU.
- Es una función de extensión en
- Un elemento modificador
- Aquí puedes implementar el comportamiento de tu modificador.
Existen varias formas de implementar un modificador personalizado según la funcionalidad necesaria. A menudo, la forma más sencilla de implementar un modificador personalizado es implementar una fábrica de modificadores personalizada que combine otras fábricas de modificadores 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.
Encadena modificadores existentes
A menudo, es posible crear modificadores personalizados con modificadores existentes. Por
ejemplo, Modifier.clip() se implementa con el graphicsLayer
modificador. Esta estrategia usa elementos modificadores existentes, y tú proporcionas tu propia fábrica de modificadores personalizada.
Antes de implementar tu propio modificador personalizado, verifica si puedes usar la misma estrategia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
O, si descubres que repites el mismo grupo de modificadores con frecuencia, puedes unirlos en tu propio modificador:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Crea 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 fábrica de modificadores componible.
El uso de una fábrica de modificadores componible para crear un modificador también te permite usar
APIs de Compose de nivel superior, como animate*AsState y otras Compose
APIs de animación respaldadas por el estado. Por ejemplo, el siguiente fragmento 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 de conveniencia para proporcionar valores predeterminados de un CompositionLocal, la forma más sencilla de implementarlo es usar una fábrica de modificadores 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 en las siguientes secciones.
Los valores de 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 donde se crean, no donde se usan. Esto puede generar resultados inesperados. Por ejemplo, considera el ejemplo de modificador local de composición mencionado anteriormente, implementado de forma ligeramente 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 tu modificador, usa un
Modifier.Node personalizado en su lugar, ya que los elementos locales de composición 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
Nunca se omiten los modificadores de fábrica componibles porque no se pueden omitir las funciones componibles que tienen valores de retorno. Esto significa que se llamará a tu función modificadora 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 componibles, se debe llamar a un modificador de fábrica componible desde la composición. Esto limita el lugar al que se puede elevar 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 fuera de las funciones componibles para permitir una reutilización más sencilla 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 del modificador personalizado 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
La implementación de un modificador personalizado con Modifier.Node consta de tres partes:
- Una
Modifier.Nodeimplementación que contiene la lógica y el estado de tu modificador. - Un
ModifierNodeElementque 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 perdurarán en varias recomposiciones, e incluso se pueden reutilizar.
En la siguiente sección, se describe cada parte y se muestra un ejemplo de cómo compilar 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. El ejemplo anterior 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 ejemplo |
Un |
||
Un |
||
La implementación de esta interfaz permite que tu |
||
Un |
||
Un |
||
Un |
||
Un |
||
Un |
||
Los |
||
Un Esto puede ser útil para componer varias implementaciones de nodos en una. |
||
Permite que las clases |
Los nodos se invalidan automáticamente cuando se llama a la actualización en su elemento correspondiente. Como nuestro ejemplo es un DrawModifierNode, cada vez que se llama a la actualización en el elemento, el nodo activa un redibujo y su color se actualiza correctamente. Es
posible inhabilitar la invalidación automática, como se detalla en la
sección Inhabilita la invalidación automática de nodos.
ModifierNodeElement
Un 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: Es la función que instancia tu nodo modificador. Se llama a este método para crear el nodo cuando se aplica tu 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 en el que ya existe este nodo, pero cambió una propiedad. Esto se determina con el métodoequalsde la clase. El nodo modificador que se creó anteriormente se envía como parámetro a la llamadaupdate. En este punto, debes actualizar las propiedades de los nodos para que correspondan con los parámetros actualizados. La capacidad de reutilizar los nodos de esta manera es clave para las mejoras de rendimiento que aportaModifier.Node. 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 igualdad con el elemento anterior muestra un valor falso.
En el ejemplo anterior, se usa una clase de datos para lograr esto. Estos métodos se usan para verificar si un nodo necesita actualizarse o no. Si tu elemento tiene propiedades que
no contribuyen a si se debe actualizar un nodo o no, o si deseas evitar
las clases de datos por motivos de compatibilidad binaria, puedes implementar manualmente
equals y hashCode, por ejemplo, el
elemento modificador de relleno.
Fábrica de modificadores
Esta es la superficie de la API pública de tu modificador. La mayoría de las implementaciones crean el elemento modificador y lo agregan a la cadena de modificadores:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Ejemplo completo
Estas tres partes se unen para crear el modificador personalizado para 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 con Modifier.Node
Cuando crees modificadores personalizados con Modifier.Node, estas son algunas situaciones comunes que puedes encontrar.
Cero parámetros
Si tu modificador no tiene parámetros, nunca necesita actualizarse y, además, no necesita ser una clase de datos. A continuación, se muestra una implementación de muestra de un modificador que aplica una cantidad fija de relleno 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) } } }
Elementos locales de composición de referencia
Los modificadores Modifier.Node no observan automáticamente los cambios en los objetos de estado de Compose, como CompositionLocal. La ventaja que tienen los modificadores Modifier.Node sobre los modificadores que solo se crean 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 donde se asigna el modificador, con currentValueOf.
Sin embargo, las instancias de nodos modificadores no observan automáticamente los cambios de estado. Para reaccionar automáticamente a un cambio local de composición, puedes leer su valor actual dentro de un alcance:
DrawModifierNode:ContentDrawScopeLayoutModifierNode:MeasureScope&IntrinsicMeasureScopeSemanticsModifierNode:SemanticsPropertyReceiver
En este ejemplo, se observa el valor de LocalContentColor para dibujar un fondo según su color. Como ContentDrawScope observa los cambios de instantáneas, 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 los cambios en LocalDensity. En el siguiente ejemplo, 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) } }
Anima un modificador
Las implementaciones de Modifier.Node tienen acceso a un coroutineScope. Esto permite
el uso de las APIs de Animatable de Compose. Por ejemplo, este fragmento modifica el CircleNode que se mostró anteriormente para que aparezca y desaparezca de forma repetida:
class CircleNode(var color: Color) : Modifier.Node(), DrawModifierNode { private lateinit var alpha: Animatable<Float, AnimationVector1D> override fun ContentDrawScope.draw() { drawCircle(color = color, alpha = alpha.value) drawContent() } override fun onAttach() { alpha = Animatable(1f) coroutineScope.launch { alpha.animateTo( 0f, infiniteRepeatable(tween(1000), RepeatMode.Reverse) ) { } } } }
Comparte el estado entre modificadores con la delegación
Los modificadores Modifier.Node pueden delegar a otros nodos. Existen muchos casos de uso para esto, como extraer implementaciones comunes en diferentes modificadores, pero también se puede usar para compartir un estado común en los modificadores.
Por ejemplo, una implementación básica de un nodo modificador en el que se puede hacer clic y que comparte datos de interacción:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Inhabilita la invalidación automática de nodos
Los nodos Modifier.Node se invalidan automáticamente cuando su ModifierNodeElement correspondiente llama a la actualización. Para los modificadores complejos, es posible que desees inhabilitar este comportamiento para obtener un control más detallado sobre cuándo tu modificador invalida las fases.
Esto es muy útil si tu modificador personalizado modifica el diseño y el dibujo. Si inhabilitas la invalidación automática, solo invalidarás el dibujo cuando cambien las propiedades relacionadas con el dibujo, como color. Esto evita invalidar el diseño y puede mejorar el rendimiento de tu modificador.
En el siguiente ejemplo, se muestra un ejemplo hipotético con un modificador que tiene una lambda color, size y onClick como propiedades. Este modificador invalida solo lo que se requiere y omite cualquier invalidación innecesaria:
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) } } }