Modifier.composed wurde in Compose 1.0 eingeführt, damit Sie über Modifikatoren auf Kompositionselemente zugreifen können. Ein wichtiger Anwendungsfall ist beispielsweise das Erstellen eines zustandsorientierten Modifikators, der sich den lokalen Zustand merkt und ihn mit anderen Modifikatoren in der Modifier.composed-Factory teilt:
// ❌ 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 ) }
Verwenden Sie Modifier.Node anstelle von Modifier.composed, da Modifier.Node die Verwaltung des Status innerhalb von Modifikatoren verbessert. Ein Modifier.Node ist ein langlebiges, zustandsorientiertes Objekt, das einmal pro Modifier.Element erstellt wird, das auf ein LayoutNode angewendet wird. Es überlebt die Neuzusammensetzung, anstatt bei jedem Durchlauf durch die Zusammensetzung neu materialisiert zu werden. Weitere Informationen dazu, warum und wie wir Modifier.Node entwickelt haben, finden Sie unter Compose Modifiers im Detail.
In diesem Dokument wird beschrieben, wie Sie von Modifier.composed zu Modifier.Node migrieren. Weitere Informationen zur allgemeinen Verwendung dieser API finden Sie unter Benutzerdefiniertes Modifikatorverhalten mit Modifier.Node implementieren.
Leistungsvorteile von Modifier.Node
Die Verwendung von Modifier.composed führt zu mehreren grundlegenden Leistungsengpässen:
- Overhead für die Statusverwaltung: Die Statusverwaltung in diesem Bereich erfordert
remember-Aufrufe und Snapshot-Statusobjekte. Dadurch wird die Slot-Tabelle mit unnötigen Kompositionsgruppen aufgebläht und die Speicherauslastung erhöht. - Aufwendiger Zugriff auf den Lebenszyklus: Für den Zugriff auf den Lebenszyklus des Modifikators müssen Effekte wie
DisposableEffectverwendet werden. Dadurch erhöht sich der Aufwand für einfachere Anwendungsfälle schnell. - Nicht überspringbar: Da der an
composedübergebene Lambda-Ausdruck einenModifierzurückgibt, kann der Compose-Compiler ihn nicht als überspringbar markieren. Er muss daher bei jeder Neuzusammensetzung des Layouts noch einmal ausgeführt werden. - Fehlerhafte Memoisierung und Gleichheit: Da die äußere Erweiterungsfunktion selbst kein
@Composableist, kann der Compiler das innere Lambda nicht memoisieren. Das führt bei jedem Aufruf zu neuen Lambda-Zuweisungen. Diese fehlende Memoisierung bricht die Gleichheit von Modifikatoren (equals) direkt, daComposedModifierLambdas nach Referenz vergleicht. Daher behandelt Compose den Modifikator bei jedem Frame als geändert, auch wenn die Parameter statisch sind. - Keine intelligente Weitergabe von Änderungen: Ohne Tracking von composable Parametern auf oberster Ebene gibt es keine Möglichkeit, neue Eingaben mit vorherigen Eingaben zu vergleichen, um Änderungen intelligent weiterzugeben.
Insgesamt fördert die Modifier.composed-API-Form das Schreiben von aufwendigem Code und verhindert, dass die Compose-Laufzeit zusätzliche Modifikatoroptimierungen anwendet.
Wichtige Migrationsschritte
Das folgende Beispiel zeigt einen typischen benutzerdefinierten Modifikator, der mit Modifier.composed implementiert wurde. Weitere Informationen finden Sie unter Benutzerdefiniertes Modifikatorverhalten mit Modifier.Node implementieren.
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 ) } }
Erstellen Sie eine benutzerdefinierte
Modifier.Node(oderDelegatingNode):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) { } }
Implementieren Sie je nach den Anforderungen Ihres benutzerdefinierten Modifikators eine oder mehrere der Hilfs-APIs von
Modifier.Node(z. B.PointerInputModifierNode, wenn Zugriff auf Zeigereingabe-APIs erforderlich ist):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() } }
Erstellen Sie einen
ModifierNodeElement, der Ihren benutzerdefinierten Knoten erstellt und aktualisiert: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 } }
Aktualisieren Sie die Modifikator-Factory so, dass sie auf
ModifierNodeElementverweist:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
Gängige Migrationsrezepte
Die folgenden Rezepte zeigen, wie Sie gängige Muster von Modifier.composed zu Modifier.Node- oder @Composable-Modifikator-Factories migrieren.
Auf ein CompositionLocal zugreifen
Muster: Lesen eines einzelnen CompositionLocal, z. B. LocalDensity, Theme oder LocalView.
Migrationspfad: Markieren Sie den Modifikator mit @Composable. Es gibt einen semantischen Unterschied zwischen der Verwendung eines composed-Modifikators und einer @Composable-Modifikator-Factory für den Zugriff auf einen CompositionLocal. Bei einer @Composable-Factory werden CompositionLocal-Werte an der Aufrufstelle der Modifikator-Factory aufgelöst. Wenn das nicht das beabsichtigte Verhalten ist, verwenden Sie eine benutzerdefinierte Modifier.Node-Implementierung, die CompositionLocals mit CompositionLocalConsumerModifierNode liest.
Weitere Informationen finden Sie unter Benutzerdefinierten Modifikator mit einer zusammensetzbaren Modifikator-Factory erstellen.
// ❌ 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()
Muster: Lesen eines CompositionLocal, das auf einen nachfolgenden Modifikator angewendet werden könnte.
Migrationspfad: Erstellen Sie einen benutzerdefinierten Modifier.Node, der CompositionLocalConsumerModifierNode implementiert und alle Funktionen der Modifikatoren kombiniert.
// ❌ 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) } } }
Auf eine nicht layoutbezogene zusammensetzbare Funktion zugreifen
Muster: Der Modifikator muss auf eine Funktion zugreifen, die mit @Composable annotiert ist und ein Objekt zurückgibt (z. B. colorResource oder ScrollableDefaults.flingBehavior).
Migrationspfad: Annotieren Sie den Modifikator mit @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) }
Auf einen Koroutinenbereich zugreifen
Muster: Modifier.composed wird verwendet, um rememberCoroutineScope auszuführen und auf ein coroutineScope-Objekt zum Starten von Koroutinen zuzugreifen.
Migrationspfad:Verwenden Sie ein benutzerdefiniertes Modifier.Node, das ein coroutineScope-Attribut hat, das an den Lebenszyklus des Modifikators gebunden ist (z. B. rememberCoroutineScope in 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() } }
Sich an einen Status erinnern
Muster: remember in Modifier.composed verwenden, um den Status über Neuzusammensetzungen hinweg zu speichern.
Migrationspfad: Modifier.Node wurde so erstellt, dass der Status auf dieselbe Weise beibehalten wird. Der Status kann wie jede andere Klasseneigenschaft mit einem klareren Lebenszyklus in einer Instanz gespeichert werden:
// ❌ 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() } }
Effekt verwenden
Muster: Effekte verwenden, um Vorgänge auszuführen, die an den Lebenszyklus der Komposition gebunden sind (z. B. wenn Modifier.composed in die Komposition eintritt oder sie verlässt).
Migrationspfad: Modifier.Node hat klare Lebenszyklus-Callbacks, mit denen dieselben Vorgänge ausgeführt werden können. Beispielsweise kann ein LaunchedEffect im Allgemeinen durch die Verwendung von coroutineScope in der Methode Modifier.Node onAttach ersetzt werden:
// ❌ 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) } } }
Animationsstatus beibehalten
Muster: Modifier.composed mit animate*AsState.
Migrationspfad: animate*AsState kann in einen benutzerdefinierten Modifikator aufgeteilt werden, der die Lebenszyklus-Callbacks überwacht und einen Animatable-Status enthält:
// ❌ 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 } } } }