Modifier.composed è stato introdotto in Compose 1.0 per consentirti di accedere
agli elementi di composizione dai modificatori. Ad esempio, un caso d'uso chiave è la creazione di un modificatore stateful che memorizza lo stato locale e lo condivide con altri modificatori nella factory Modifier.composed:
// ❌ BAD: Using Modifier.composed is no longer recommended fun Modifier.pressScale( pressedScale: Float = 0.95f, onClick: () -> Unit ): Modifier = composed( inspectorInfo = debugInspectorInfo { name = "pressScale" properties["pressedScale"] = pressedScale } ) { val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val scale by animateFloatAsState( targetValue = if (isPressed) pressedScale else 1f, animationSpec = spring(), label = "pressScale" ) this .graphicsLayer { scaleX = scale scaleY = scale } .clickable( interactionSource = interactionSource, indication = null, onClick = onClick ) }
Utilizza Modifier.Node anziché Modifier.composed, in quanto
Modifier.Node migliora la gestione dello stato all'interno dei modificatori. Un
Modifier.Node è un oggetto stateful di lunga durata creato una volta per
Modifier.Element applicato a un LayoutNode e sopravvive alla ricomposizione
anziché essere materializzato nuovamente tramite la composizione a ogni passaggio. Per saperne di più
sul perché e su come abbiamo progettato Modifier.Node, consulta
Analisi approfondita dei modificatori di composizione.
Questo documento descrive come eseguire la migrazione da Modifier.composed a
Modifier.Node. Per ulteriori informazioni sull'utilizzo generale di questa API, consulta
Implementare il comportamento dei modificatori personalizzati utilizzando Modifier.Node.
Vantaggi in termini di prestazioni di Modifier.Node
L'utilizzo di Modifier.composed introduce diversi colli di bottiglia fondamentali
per il rendimento:
- Overhead di gestione dello stato: la gestione dello stato in questo ambito richiede
chiamate
remembere oggetti di stato snapshot, il che gonfia la tabella degli slot con gruppi di composizione non necessari e aumenta la pressione sulla memoria. - Accesso costoso al ciclo di vita:l'accesso al ciclo di vita del modificatore richiede
l'utilizzo di effetti come
DisposableEffect, che aumenta rapidamente il lavoro richiesto per i casi d'uso più semplici. - Mancanza di possibilità di ignorare:poiché la lambda passata a
composedrestituisce unModifier, il compilatore Compose non può contrassegnarla come ignorabile, il che lo costringe a rieseguirla ogni volta che il layout viene ricomposto. - Memorizzazione e uguaglianza interrotte:poiché la funzione di estensione esterna
non è una
@Composable, il compilatore non può memorizzare la lambda interna, con conseguenti allocazioni lambda nuove a ogni chiamata. Questa mancanza di memoizzazione interrompe direttamente l'uguaglianza dei modificatori (equals), poichéComposedModifierconfronta le espressioni lambda per riferimento. Di conseguenza, Compose considera il modificatore come modificato a ogni frame anche quando i parametri sono statici. - Nessuna propagazione intelligente delle modifiche: senza il monitoraggio dei parametri componibili di primo livello, non è possibile confrontare i nuovi input con quelli precedenti per la propagazione intelligente delle modifiche.
Nel complesso, la forma dell'API Modifier.composed incoraggia la scrittura di codice costoso
e impedisce al runtime di Compose di applicare ulteriori ottimizzazioni
dei modificatori.
Passaggi principali per la migrazione
L'esempio seguente mostra un tipico modificatore personalizzato implementato con
Modifier.composed. Per maggiori informazioni, vedi
Implementare il comportamento dei modificatori personalizzati utilizzando Modifier.Node.
fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = composed { val density = LocalDensity.current val strokePx = with(density) { thickness.toPx() } // Drives how much of the underline is drawn: 0f -> 1f val progress = remember { Animatable(0f) } LaunchedEffect(color, thickness) { progress.snapTo(0f) progress.animateTo( targetValue = 1f, animationSpec = tween(durationMillis = animationDurationMillis) ) } drawBehind { val y = size.height - strokePx / 2 drawLine( color = color, start = Offset(0f, y), end = Offset(size.width * progress.value, y), strokeWidth = strokePx ) } }
Crea un
Modifier.Node(oDelegatingNode) personalizzato:private class UnderlineNode( private var color: Color, private var thickness: Dp, private var animationDurationMillis: Int ) : Modifier.Node() { fun update(color: Color, thickness: Dp, durationMillis: Int) { } }
Implementa una o più API ausiliarie di
Modifier.Node, a seconda delle esigenze del modificatore personalizzato (ad esempio,PointerInputModifierNodese deve accedere alle API di input del puntatore):private class UnderlineNode( private var color: Color, private var thickness: Dp, private var animationDurationMillis: Int ) : Modifier.Node(), DrawModifierNode { private val progress = Animatable(0f) private var animationJob: Job? = null override fun onAttach() { restartAnimation() } fun update(color: Color, thickness: Dp, durationMillis: Int) { val needsRestart = this.color != color || this.thickness != thickness this.color = color this.thickness = thickness this.animationDurationMillis = durationMillis if (needsRestart) restartAnimation() } private fun restartAnimation() { animationJob?.cancel() animationJob = coroutineScope.launch { progress.snapTo(0f) progress.animateTo(1f, tween(animationDurationMillis)) } } override fun ContentDrawScope.draw() { val strokePx = thickness.toPx() val y = size.height - strokePx / 2 drawLine( color = color, start = Offset(0f, y), end = Offset(size.width * progress.value, y), strokeWidth = strokePx ) drawContent() } }
Crea un
ModifierNodeElementche crei e aggiorni il nodo personalizzato:private class UnderlineElement( private val color: Color, private val thickness: Dp, private val animationDurationMillis: Int ) : ModifierNodeElement<UnderlineNode>() { override fun create() = UnderlineNode(color, thickness, animationDurationMillis) override fun update(node: UnderlineNode) { node.update(color, thickness, animationDurationMillis) } override fun InspectorInfo.inspectableProperties() { name = "underline" properties["color"] = color properties["thickness"] = thickness properties["animationDurationMillis"] = animationDurationMillis } override fun hashCode(): Int { var result = color.hashCode() result = 31 * result + thickness.hashCode() result = 31 * result + animationDurationMillis.hashCode() return result } override fun equals(other: Any?): Boolean { if (this === other) return true val otherElement = other as? UnderlineElement ?: return false return color == otherElement.color && thickness == otherElement.thickness && animationDurationMillis == otherElement.animationDurationMillis } }
Aggiorna la fabbrica di modificatori in modo che punti a
ModifierNodeElement:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
Ricette di migrazione comuni
Le seguenti ricette mostrano come eseguire la migrazione di pattern comuni da Modifier.composed
alle fabbriche di modificatori Modifier.Node o @Composable.
Accedere a un CompositionLocal
Pattern:lettura di un singolo CompositionLocal come LocalDensity,
Theme o LocalView.
Percorso di migrazione: contrassegna il modificatore con @Composable. Esiste una differenza semantica
tra l'utilizzo di un modificatore composed e di un modificatore @Composable
factory per accedere a un CompositionLocal. Con una factory @Composable,
i valori CompositionLocal vengono risolti nel sito di chiamata della factory del modificatore. Se questo non è il comportamento previsto, utilizza un'implementazione personalizzata di Modifier.Node
che legge i CompositionLocal utilizzando
CompositionLocalConsumerModifierNode.
Per saperne di più, consulta Creare un modificatore personalizzato utilizzando una factory di modificatori componibili.
// ❌ BAD: Using Modifier.composed to read a single CompositionLocal fun Modifier.themedContainerBorder(): Modifier = composed { Modifier.border( BorderStroke( width = 2.dp, color = LocalColorScheme.current.primaryColor, ) ) .clipToBounds() }
// ✅ GOOD: If the modifier is @Composable, it should be able to access the locals. @Composable fun Modifier.themedContainerBorder() = this then Modifier.border( BorderStroke( width = 2.dp, color = MyTheme.mainColor, ) ) .clipToBounds()
Pattern: lettura di un CompositionLocal che potrebbe essere applicato a un modificatore successivo.
Percorso di migrazione:crea un Modifier.Node personalizzato che implementi
CompositionLocalConsumerModifierNode e combini tutte le funzionalità
dei modificatori.
// ❌ BAD: Using Modifier.composed to read a CompositionLocal then using it in another modifier. fun Modifier.adaptiveAccessibilityPadding(basePadding: Dp): Modifier = composed { // Reading LocalThemePadding.current.small (CompositionLocal) val extraPadding = LocalThemePadding.current.small Modifier.padding(basePadding + extraPadding) }
// ✅ GOOD: A custom Modifier that combines the capabilities of both (layout and composition local reader) modifiers. fun Modifier.adaptiveAccessibilityPadding(basePadding: Dp): Modifier = this.then(AdaptivePaddingElement(basePadding)) private data class AdaptivePaddingElement( val basePadding: Dp, ) : ModifierNodeElement<AdaptivePaddingNode>() { override fun create() = AdaptivePaddingNode(basePadding) override fun update(node: AdaptivePaddingNode) { node.basePadding = basePadding } override fun InspectorInfo.inspectableProperties() { name = "adaptiveAccessibilityPadding" properties["basePadding"] = basePadding } } private class AdaptivePaddingNode( var basePadding: Dp, ) : Modifier.Node(), LayoutModifierNode, CompositionLocalConsumerModifierNode { override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { val extraPadding = currentValueOf(LocalThemePadding).small val total = (basePadding + extraPadding).roundToPx() val horizontal = total * 2 val vertical = total * 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(total, total) } } }
Accedere a una funzione componibile non di layout
Pattern: il modificatore deve accedere a una funzione annotata con
@Composable e restituire un oggetto (ad esempio colorResource o
ScrollableDefaults.flingBehavior).
Percorso di migrazione:annota il modificatore con @Composable.
// ❌ BAD: Using Modifier.composed to access a composable function such as colorResource fun Modifier.niceBackground() = composed { // Reading composable function colorResource val gradientColor1 = colorResource(R.color.my_special_color) background(color = gradientColor1, shape = CircleShape) }
// ✅ GOOD: A modifier can be annotation with @Composable to reference composable functions. @Composable // Modifier can be Composable itself. private fun Modifier.niceBackground(): Modifier { val gradientColor1 = colorResource(R.color.my_special_color) return this.background(color = gradientColor1, shape = CircleShape) }
Accedere a un ambito di coroutine
Pattern: Modifier.composed viene utilizzato per eseguire rememberCoroutineScope per
accedere a un oggetto coroutineScope per avviare le coroutine.
Percorso di migrazione:utilizza un Modifier.Node personalizzato, che ha una proprietà coroutineScope
collegata al ciclo di vita del modificatore (ad esempio rememberCoroutineScope
all'interno di Modifier.composed):
// ❌ BAD: Using Modifier.composed to get access to a coroutine scope. fun Modifier.onClickAsyncComposed(onClick: suspend () -> Unit): Modifier = composed { val scope = rememberCoroutineScope() Modifier.pointerInput(onClick) { detectTapGestures { // Needs a coroutine scope to launch suspend lambda. scope.launch { onClick() } } } }
// ✅ GOOD: A custom Modifier.Node has a scoped (modifier lifecycle) coroutineScope that can be used to launch async work. fun Modifier.onClickAsync(onClick: suspend () -> Unit): Modifier = this.then(OnClickAsyncElement(onClick)) private data class OnClickAsyncElement(val onClick: suspend () -> Unit) : ModifierNodeElement<OnClickAsyncNode>() { override fun create(): OnClickAsyncNode = OnClickAsyncNode(onClick) override fun update(node: OnClickAsyncNode) { node.update(onClick) } override fun InspectorInfo.inspectableProperties() { name = "onClickAsync" properties["onClick"] = onClick } } private class OnClickAsyncNode(private var onClick: suspend () -> Unit) : DelegatingNode(), PointerInputModifierNode { private val pointerInputNode = delegate( SuspendingPointerInputModifierNode { detectTapGestures { // Modifier.Node provides `coroutineScope` directly. coroutineScope.launch { onClick() } } } ) fun update(onClick: suspend () -> Unit) { if (this.onClick != onClick) { this.onClick = onClick pointerInputNode.resetPointerInputHandler() } } override fun onPointerEvent( pointerEvent: PointerEvent, pass: PointerEventPass, bounds: IntSize, ) { pointerInputNode.onPointerEvent(pointerEvent, pass, bounds) } override fun onCancelPointerInput() { pointerInputNode.onCancelPointerInput() } }
Ricordare uno stato
Pattern:utilizzo di remember in Modifier.composed per salvare lo stato tra le ricomposizioni.
Percorso di migrazione: Modifier.Node è stato creato per mantenere lo stato nello stesso modo. Lo stato può essere mantenuto all'interno di un'istanza, proprio come qualsiasi altra proprietà della classe
con un ciclo di vita più chiaro:
// ❌ BAD: Using Modifier.composed to make the modifier stateful. fun Modifier.tapCountHighlightComposed(colors: List<Color>): Modifier = composed { // 1. Must use `remember` so `tapCount` isn't reset to 0 on every recomposition var tapCount by remember { mutableIntStateOf(0) } Modifier .pointerInput(colors) { detectTapGestures { tapCount++ } } .drawBehind { drawRect(colors[tapCount % colors.size]) } }
// ✅ GOOD: Modifier.Node is the recommended way of creating stateful modifiers. fun Modifier.tapCountHighlight(colors: List<Color>): Modifier = this then TapCountHighlightElement(colors) private data class TapCountHighlightElement( val colors: List<Color>, ) : ModifierNodeElement<TapCountHighlightNode>() { override fun create() = TapCountHighlightNode(colors) override fun update(node: TapCountHighlightNode) { node.updateColors(colors) } override fun InspectorInfo.inspectableProperties() { name = "tapCountHighlight" properties["colors"] = colors } } private class TapCountHighlightNode( private var colors: List<Color>, ) : DelegatingNode(), DrawModifierNode { private var tapCount = 0 // Stateful modifier, this property will survive recompositions since Modifier.Nodes are held in the modifier tree. private val pointerInputNode = delegate( SuspendingPointerInputModifierNode { detectTapGestures { tapCount++ invalidateDraw() } } ) override fun ContentDrawScope.draw() { drawRect(colors[tapCount % colors.size]) drawContent() } fun updateColors(colors: List<Color>) { this.colors = colors invalidateDraw() } }
Utilizzare un effetto
Pattern: utilizzo di effetti per eseguire operazioni legate al ciclo di vita della composizione (ad esempio, quando Modifier.composed è entrato o uscito dalla composizione).
Percorso di migrazione:Modifier.Node ha callback del ciclo di vita chiari che possono essere
utilizzati per eseguire le stesse operazioni. Ad esempio, un LaunchedEffect può
generalmente essere sostituito dall'utilizzo di coroutineScope all'interno del
metodo Modifier.Node onAttach:
// ❌ BAD: Using Modifier.composed to launch/run an effect. fun Modifier.logImpressionComposed( targetId: String, onLog: suspend (targetId: String) -> Unit, ): Modifier = composed { // LaunchedEffect is tied to Composition lifecycle LaunchedEffect(targetId) { onLog(targetId) } this }
// ✅ GOOD: Modifier.Node has lifecycle callbacks (e.g onAttach, onDetach) that can be used to emulate effects behaviors. fun Modifier.logImpression(targetId: String, onLog: suspend (targetId: String) -> Unit): Modifier = this.then(LogImpressionElement(targetId, onLog)) private data class LogImpressionElement( val targetId: String, val onLog: suspend (targetId: String) -> Unit, ) : ModifierNodeElement<LogImpressionNode>() { override fun create(): LogImpressionNode = LogImpressionNode(targetId, onLog) override fun update(node: LogImpressionNode) { node.update(targetId, onLog) } override fun InspectorInfo.inspectableProperties() { name = "logImpression" properties["targetId"] = targetId } } private class LogImpressionNode( var targetId: String, var onLog: suspend (targetId: String) -> Unit, ) : Modifier.Node() { private var job: Job? = null override fun onAttach() { super.onAttach() runEffect() // Uses onAttach to track modifier lifecycle. } fun update(targetId: String, onLog: suspend (targetId: String) -> Unit) { // Re-run the effect if the key (`targetId`) changed if (this.targetId != targetId) { runEffect() } this.targetId = targetId this.onLog = onLog } private fun runEffect() { job?.cancel() job = coroutineScope.launch { onLog(targetId) } } }
Mantenere uno stato di animazione
Pattern: Modifier.composed utilizzando animate*AsState.
Percorso di migrazione:animate*AsState può essere suddiviso in un modificatore personalizzato
che monitora i callback del ciclo di vita e contiene uno stato Animatable:
// ❌ BAD: Using Modifier.composed to save an animation state. fun Modifier.fadeInOnHoverComposed(isHovered: Boolean): Modifier = composed { val alpha by animateFloatAsState( targetValue = if (isHovered) 1f else 0.4f, animationSpec = tween(durationMillis = 300), label = "alphaAnimation", ) Modifier.graphicsLayer { this.alpha = alpha } }
// ✅ GOOD: Animation state can be saved in Modifier.Node like other types of stateful implementations. fun Modifier.fadeInOnHover(isHovered: Boolean): Modifier = this.then(FadeInOnHoverElement(isHovered)) private data class FadeInOnHoverElement(val isHovered: Boolean) : ModifierNodeElement<FadeInOnHoverNode>() { override fun create(): FadeInOnHoverNode = FadeInOnHoverNode(isHovered) override fun update(node: FadeInOnHoverNode) { node.update(isHovered) } override fun InspectorInfo.inspectableProperties() { name = "fadeInOnHover" properties["isHovered"] = isHovered } } private class FadeInOnHoverNode(var isHovered: Boolean) : Modifier.Node(), LayoutModifierNode { // 1. Persistent Animatable field on the Node instance private val alphaAnimatable = Animatable(if (isHovered) 1f else 0.4f) override fun onAttach() { super.onAttach() startAnimation(isHovered) } // 2. Trigger animation imperatively when `isHovered` argument changes fun update(isHovered: Boolean) { if (this.isHovered != isHovered) { this.isHovered = isHovered if (isAttached) { startAnimation(isHovered) } } } private fun startAnimation(hovered: Boolean) { val targetAlpha = if (hovered) 1f else 0.4f // Use Node's built-in coroutineScope coroutineScope.launch { alphaAnimatable.animateTo( targetValue = targetAlpha, animationSpec = tween(durationMillis = 300), ) } } override fun MeasureScope.measure( measurable: Measurable, constraints: Constraints, ): MeasureResult { val placeable = measurable.measure(constraints) return layout(placeable.width, placeable.height) { // Read current animation value during layout placement layer placeable.placeWithLayer(0, 0) { alpha = alphaAnimatable.value } } } }