O Compose oferece muitos modificadores para comportamentos comuns prontos para uso, mas você também pode criar modificadores personalizados.
Os modificadores têm várias partes:
- Uma fábrica de modificadores
- Essa é uma função de extensão em
Modifier
, que fornece uma API idiomática para o modificador e permite que ele seja facilmente encadeado. A fábrica de modificadores produz os elementos modificadores usados pelo Compose para modificar a interface.
- Essa é uma função de extensão em
- Um elemento modificador
- É aqui que você pode implementar o comportamento do seu modificador.
Há várias maneiras de implementar um modificador personalizado, dependendo da
funcionalidade necessária. Muitas vezes, a maneira mais fácil de implementar um modificador personalizado é
simplesmente implementar uma fábrica de modificadores personalizados que combina outras fábricas
de modificadores já definidas. Se você precisar de um comportamento mais personalizado, implemente o
elemento modificador usando as APIs Modifier.Node
, que são de nível inferior, mas
oferecem mais flexibilidade.
Encadear modificadores atuais
Muitas vezes, é possível criar modificadores personalizados usando apenas os
existentes. Por exemplo, Modifier.clip()
é implementado usando o
modificador graphicsLayer
. Essa estratégia usa elementos modificadores já existentes e você
fornece sua própria fábrica de modificadores personalizados.
Antes de implementar seu próprio modificador personalizado, veja se você pode usar a mesma estratégia.
fun Modifier.clip(shape: Shape) = graphicsLayer(shape = shape, clip = true)
Ou, se você achar que está repetindo o mesmo grupo de modificadores com frequência, poderá envolvê-los no seu próprio modificador:
fun Modifier.myBackground(color: Color) = padding(16.dp) .clip(RoundedCornerShape(8.dp)) .background(color)
Criar um modificador personalizado usando uma fábrica de modificadores combináveis
Você também pode criar um modificador personalizado usando uma função combinável para transmitir valores a um modificador já existente. Isso é conhecido como fábrica de modificadores combináveis.
O uso de uma fábrica de modificadores combináveis para criar um modificador também permite usar
APIs do Compose de nível mais alto, como animate*AsState
e outras APIs
de animação com base em estado do Compose. Por exemplo, o snippet abaixo mostra um
modificador que anima uma mudança Alfa quando ativado/desativado:
@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 } }
Se o modificador personalizado for um método conveniente para fornecer valores padrão de um
CompositionLocal
, a maneira mais fácil de implementar isso é usando uma fábrica de modificadores
combináveis:
@Composable fun Modifier.fadedBackground(): Modifier { val color = LocalContentColor.current return this then Modifier.background(color.copy(alpha = 0.5f)) }
Essa abordagem tem algumas ressalvas detalhadas abaixo.
Os valores de CompositionLocal
são resolvidos no local de chamada da fábrica de modificadores
Ao criar um modificador personalizado usando uma fábrica de modificadores combináveis, os locais de composição usam o valor da árvore de composição em que são criados, e não usados. Isso pode levar a resultados inesperados. Por exemplo, considere o exemplo de modificador local de composição acima, implementado de maneira um pouco diferente usando uma função combinável:
@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) } } }
Se não for assim que você espera que o modificador funcione, use um
Modifier.Node
personalizado, já que os locais de composição vão ser
resolvidos corretamente no local de uso e poderão ser elevados com segurança.
Os modificadores de função combinável nunca são pulados
Os modificadores de fábrica que podem ser compostos nunca são ignorados porque as funções combináveis que têm valores de retorno não podem ser ignoradas. Isso significa que a função modificadora será chamada em cada recomposição, o que pode custar caro se ela for recomposta com frequência.
Modificadores de funções combináveis precisam ser chamados dentro de uma função combinável
Como todas as funções combináveis, um modificador de fábrica combinável precisa ser chamado dentro da composição. Isso limita onde um modificador pode ser elevado, já que nunca pode ser elevado para fora da composição. Em comparação, as fábricas de modificadores não combináveis podem ser elevadas para fora das funções combináveis para facilitar a reutilização e melhorar a performance:
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 }
Implementar o comportamento do modificador personalizado usando Modifier.Node
A Modifier.Node
é uma API de nível inferior para criar modificadores no Compose. Essa
é a mesma API em que o Compose implementa os próprios modificadores e é a maneira
de criar modificadores personalizados com melhor desempenho.
Implementar um modificador personalizado usando Modifier.Node
A implementação de um modificador personalizado usando Modifier.Node se divide em três partes:
- Uma implementação de
Modifier.Node
que contém a lógica e o estado do modificador. - Um
ModifierNodeElement
que cria e atualiza instâncias de nó do modificador. - Uma fábrica de modificadores opcional, conforme detalhado acima.
As classes ModifierNodeElement
não têm estado, e novas instâncias são alocadas a cada
recomposição, enquanto as classes Modifier.Node
podem ter estado, sobreviver
em várias recomposições e até mesmo ser reutilizadas.
A seção abaixo descreve cada parte e mostra um exemplo de como criar um modificador personalizado para desenhar um círculo.
Modifier.Node
A implementação de Modifier.Node
(neste exemplo, CircleNode
) implementa
a
funcionalidade do modificador personalizado.
// Modifier.Node private class CircleNode(var color: Color) : DrawModifierNode, Modifier.Node() { override fun ContentDrawScope.draw() { drawCircle(color) } }
Neste exemplo, ele desenha o círculo com a cor transmitida para a função modificadora.
Um nó implementa Modifier.Node
, bem como zero ou mais tipos de nó. Há
diferentes tipos de nó com base na funcionalidade necessária para seu modificador. O
exemplo acima precisa ser capaz de desenhar, então implementa DrawModifierNode
, o que
permite que ele substitua o método de desenho.
Os tipos disponíveis são os seguintes:
Nó |
Uso |
Link de exemplo |
Um |
||
Um |
||
A implementação dessa interface permite que o |
||
Um |
||
Uma |
||
Uma |
||
Um |
||
Um |
||
|
||
Uma Isso pode ser útil para compor várias implementações de nó em uma. |
||
Permite que as classes |
Os nós são invalidados automaticamente quando a atualização é chamada no elemento correspondente. Como o exemplo é uma DrawModifierNode
, sempre que uma atualização é chamada no
elemento, o nó aciona um redesenho, e a cor dele é atualizada corretamente. É
possível desativar a invalidação automática, conforme detalhado abaixo.
ModifierNodeElement
Um ModifierNodeElement
é uma classe imutável que contém os dados para criar ou
atualizar seu 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 } }
As implementações de ModifierNodeElement
precisam substituir os seguintes métodos:
create
: é a função que instancia o nó modificador. Ele é chamado para criar o nó quando o modificador é aplicado pela primeira vez. Normalmente, isso equivale a construir o nó e configurá-lo com os parâmetros que foram transmitidos para a fábrica de modificadores.update
: essa função é chamada sempre que esse modificador é fornecido no mesmo local em que o nó já existe, mas uma propriedade mudou. Isso é determinado pelo métodoequals
da classe. O nó modificador que foi criado anteriormente é enviado como um parâmetro para a chamadaupdate
. Nesse ponto, atualize as propriedades dos nós para corresponder aos parâmetros atualizados. A capacidade de reutilizar os nós dessa maneira é fundamental para os ganhos de desempenho que oModifier.Node
traz. Portanto, é necessário atualizar o nó atual em vez de criar um novo no métodoupdate
. Em nosso exemplo de círculo, a cor do nó é atualizada.
Além disso, as implementações de ModifierNodeElement
também precisam implementar equals
e hashCode
. O update
só será chamado se uma comparação de igualdade com o
elemento anterior retornar "false".
O exemplo acima usa uma classe de dados para fazer isso. Esses métodos são usados para
verificar se um nó precisa ser atualizado ou não. Se o elemento tiver propriedades que não
contribuam para a atualização de um nó ou se você quiser evitar classes
de dados por motivos de compatibilidade binária, implemente manualmente equals
e hashCode
, por exemplo, o elemento modificador de padding.
Fábrica de modificadores
Esta é a superfície da API pública do seu modificador. A maioria das implementações simplesmente cria o elemento modificador e o adiciona à cadeia de modificadores:
// Modifier factory fun Modifier.circle(color: Color) = this then CircleElement(color)
Exemplo completo
Essas três partes se unem para criar o modificador personalizado e desenhar um círculo
usando as APIs 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) } }
Situações comuns com o uso de Modifier.Node
Aqui estão algumas situações comuns que você pode
encontrar ao criar modificadores personalizados com Modifier.Node
.
Nenhum parâmetro
Se o modificador não tiver parâmetros, ele nunca vai precisar ser atualizado e, além disso, não precisa ser uma classe de dados. Confira um exemplo de implementação de um modificador que aplica uma quantidade fixa de padding a um elemento combinável:
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) } } }
Referência a locais de composição
Os modificadores Modifier.Node
não observam automaticamente as mudanças nos objetos de estado
do Compose, como CompositionLocal
. A vantagem dos modificadores Modifier.Node
em relação aos
modificadores recém-criados com uma fábrica combinável é que eles podem ler
o valor do local de composição de onde o modificador é usado na árvore de
interface, e não de onde o modificador está alocado, usando currentValueOf
.
No entanto, as instâncias de nó modificador não observam automaticamente as mudanças de estado. Para reagir automaticamente a uma mudança local de composição, leia o valor atual dela em um escopo:
DrawModifierNode
:ContentDrawScope
LayoutModifierNode
:MeasureScope
eIntrinsicMeasureScope
SemanticsModifierNode
:SemanticsPropertyReceiver
Este exemplo observa o valor de LocalContentColor
para desenhar um plano de fundo com base
na cor. Como ContentDrawScope
observa mudanças no snapshot, ele
é redesenhado automaticamente quando o valor de LocalContentColor
muda:
class BackgroundColorConsumerNode : Modifier.Node(), DrawModifierNode, CompositionLocalConsumerModifierNode { override fun ContentDrawScope.draw() { val currentColor = currentValueOf(LocalContentColor) drawRect(color = currentColor) drawContent() } }
Para reagir a mudanças de estado fora de um escopo e atualizar automaticamente o
modificador, use um ObserverModifierNode
.
Por exemplo, o Modifier.scrollable
usa essa técnica para
observar mudanças em LocalDensity
. Confira abaixo um exemplo 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 animação
As implementações de Modifier.Node
têm acesso a um coroutineScope
. Isso permite o
uso das APIs Compose Animatable. Por exemplo, este snippet modifica o
CircleNode
acima para aparecer e desaparecer repetidamente:
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) ) { } } } }
Como compartilhar estado entre modificadores usando delegação
Os modificadores Modifier.Node
podem delegar a outros nós. Há muitos casos de uso
para isso, como a extração de implementações comuns em diferentes modificadores,
mas ela também pode ser usada para compartilhar um estado comum entre modificadores.
Por exemplo, uma implementação básica de um nó modificador clicável que compartilha dados de interação:
class ClickableNode : DelegatingNode() { val interactionData = InteractionData() val focusableNode = delegate( FocusableNode(interactionData) ) val indicationNode = delegate( IndicationNode(interactionData) ) }
Como desativar a invalidação automática do nó
Os nós Modifier.Node
são invalidados automaticamente quando as chamadas ModifierNodeElement
correspondentes são atualizadas. Às vezes, em um modificador mais complexo, você pode
querer desativar esse comportamento para ter um controle mais refinado sobre quando
o modificador invalida fases.
Isso pode ser útil principalmente se o modificador personalizado modificar o layout e
o desenho. Desativar a invalidação automática permite apenas invalidar o desenho quando
somente propriedades relacionadas a desenhos, como color
, mudar, e não invalidar o layout.
Isso pode melhorar o desempenho do seu modificador.
Um exemplo hipotético é mostrado abaixo com um modificador que tem uma lambda color
,
size
e onClick
como propriedades. Esse modificador apenas invalida o
necessário e ignora qualquer invalidação que não seja:
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) } } }