Modifier.composed wprowadzono w Compose 1.0, aby umożliwić dostęp do elementów kompozycji z modyfikatorów. Jednym z głównych przypadków użycia jest tworzenie modyfikatora stanu, który zapamiętuje stan lokalny i udostępnia go innym modyfikatorom w fabryce 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 ) }
Używaj Modifier.Node zamiast Modifier.composed, ponieważ Modifier.Node poprawia sposób zarządzania stanem w modyfikatorach. Modifier.Node to długotrwały obiekt stanowy, który jest tworzony raz na Modifier.Element zastosowany do LayoutNode i przetrwa rekompozycję, zamiast być ponownie tworzony podczas każdego przejścia. Więcej informacji o tym, dlaczego i jak zaprojektowaliśmy Modifier.Node, znajdziesz w artykule Szczegółowe informacje o modyfikatorach Compose.
Z tego dokumentu dowiesz się, jak przeprowadzić migrację z Modifier.composed naModifier.Node. Więcej informacji o ogólnym korzystaniu z tego interfejsu API znajdziesz w artykule Implementowanie niestandardowego działania modyfikatora za pomocą Modifier.Node.
Korzyści związane z wydajnością Modifier.Node
Użycie Modifier.composed powoduje kilka podstawowych wąskich gardeł wydajności:
- Obciążenie związane z zarządzaniem stanem: zarządzanie stanem w tym zakresie wymaga wywołań
rememberi obiektów stanu migawki, co powoduje powiększenie tabeli slotów o niepotrzebne grupy kompozycji i zwiększa obciążenie pamięci. - Dostęp do cyklu życia jest kosztowny: dostęp do cyklu życia modyfikatora wymaga użycia efektów takich jak
DisposableEffect, co szybko zwiększa nakład pracy w przypadku prostszych zastosowań. - Brak możliwości pominięcia: ponieważ lambda przekazana do funkcji
composedzwraca wartośćModifier, kompilator Compose nie może oznaczyć jej jako możliwej do pominięcia, co wymusza ponowne wykonanie za każdym razem, gdy układ jest ponownie komponowany. - Nieprawidłowe zapamiętywanie i równość: ponieważ zewnętrzna funkcja rozszerzająca nie jest
@Composable, kompilator nie może zapamiętać wewnętrznej lambdy, co powoduje przydzielanie nowych lambd przy każdym wywołaniu. Brak zapamiętywania bezpośrednio narusza równość modyfikatorów (equals), ponieważComposedModifierporównuje lambdy przez odwołanie. W związku z tym Compose traktuje modyfikator jako zmieniony w każdej klatce, nawet jeśli parametry są statyczne. - Brak inteligentnego propagowania zmian: bez śledzenia parametrów kompozycyjnych najwyższego poziomu nie ma możliwości porównania nowych danych wejściowych z poprzednimi w celu inteligentnego propagowania zmian.
Ogólnie rzecz biorąc, kształt interfejsu API Modifier.composed zachęca do pisania kodu o wysokich kosztach obliczeniowych i uniemożliwia środowisku wykonawczemu Compose stosowanie dodatkowych optymalizacji modyfikatorów.
Główne etapy migracji
Poniższy przykład pokazuje typowy modyfikator niestandardowy zaimplementowany za pomocą parametru Modifier.composed. Więcej informacji znajdziesz w artykule Implementowanie niestandardowego działania modyfikatora za pomocą 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 ) } }
Utwórz niestandardowy
Modifier.Node(lubDelegatingNode):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) { } }
Zaimplementuj co najmniej 1 interfejs API pomocniczy z
Modifier.Node, w zależności od potrzeb niestandardowego modyfikatora (np.PointerInputModifierNode, jeśli potrzebuje on dostępu do interfejsów API wejścia wskaźnika):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() } }
Utwórz
ModifierNodeElement, który tworzy i aktualizuje węzeł niestandardowy: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 } }
Zaktualizuj fabrykę modyfikatorów, aby wskazywała na
ModifierNodeElement:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
Typowe przepisy migracji
Poniższe przepisy pokazują, jak migrować typowe wzorce z Modifier.composed do fabryk modyfikatorów Modifier.Node lub @Composable.
Uzyskiwanie dostępu do elementu CompositionLocal
Wzorzec: odczytanie pojedynczego znaku CompositionLocal, np. LocalDensity, Theme lub LocalView.
Ścieżka migracji: oznacz modyfikator symbolem @Composable. Istnieje różnica semantyczna między użyciem modyfikatora composed a modyfikatora @Composable do uzyskania dostępu do CompositionLocal. W przypadku modyfikatora @Composable wartości CompositionLocal są rozwiązywane w miejscu wywołania modyfikatora. Jeśli nie jest to oczekiwane działanie, użyj niestandardowej implementacji Modifier.Node, która odczytuje CompositionLocal za pomocą CompositionLocalConsumerModifierNode.
Więcej informacji znajdziesz w artykule Tworzenie niestandardowego modyfikatora za pomocą fabryki modyfikatorów kompozycyjnych.
// ❌ 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()
Wzorzec: odczytywanie CompositionLocal, które może być zastosowane do kolejnego modyfikatora.
Ścieżka migracji: utwórz niestandardowy Modifier.Node, który implementuje CompositionLocalConsumerModifierNode i łączy wszystkie możliwości modyfikatorów.
// ❌ 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) } } }
Dostęp do funkcji typu „composable”, która nie jest elementem układu
Wzorzec: modyfikator musi mieć dostęp do funkcji, która jest oznaczona adnotacją @Composable i zwraca obiekt (np. colorResource lub ScrollableDefaults.flingBehavior).
Ścieżka migracji: dodaj do modyfikatora adnotację @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) }
Dostęp do zakresu korutyny
Wzorzec: Modifier.composed służy do wykonywania rememberCoroutineScope w celu uzyskania dostępu do obiektu coroutineScope na potrzeby uruchamiania korutyn.
Ścieżka migracji: użyj niestandardowego Modifier.Node, który ma właściwość coroutineScope
powiązaną z cyklem życia modyfikatora (np. rememberCoroutineScope
w 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() } }
Zapamiętywanie stanu
Wzorzec: używanie remember w Modifier.composed do zapisywania stanu między ponownymi kompozycjami.
Ścieżka migracji: Modifier.Node został zaprojektowany tak, aby przechowywać stan w ten sam sposób. Stan może być przechowywany w instancji, tak jak każda inna właściwość klasy, z bardziej przejrzystym cyklem życia:
// ❌ 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() } }
Używanie efektu
Wzorzec: używanie efektów do wykonywania operacji powiązanych z cyklem życia kompozycji (np. gdy Modifier.composed wchodzi do kompozycji lub z niej wychodzi).
Ścieżka migracji: Modifier.Node ma jasne wywołania zwrotne cyklu życia, których można używać do wykonywania tych samych operacji. Na przykład LaunchedEffect można zwykle zastąpić użyciem coroutineScope w metodzie 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) } } }
Wstrzymywanie stanu animacji
Wzorzec: Modifier.composed za pomocą animate*AsState.
Ścieżka migracji: animate*AsState można ją podzielić na niestandardowy modyfikator, który monitoruje wywołania zwrotne cyklu życia i przechowuje Animatable stan:
// ❌ 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 } } } }