Modifier.composed, değiştiricilerden oluşturma öğelerine erişmenize olanak tanımak için Compose 1.0'da kullanıma sunuldu. Örneğin, önemli bir kullanım alanı, yerel durumu hatırlayan ve Modifier.composed fabrikasındaki diğer değiştiricilerle paylaşan durum bilgili bir değiştirici oluşturmaktır:
// ❌ 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 ) }
Modifier.composed yerine Modifier.Node kullanın. Modifier.Node, değiştiricilerde durumun yönetilme şeklini iyileştirir. Modifier.Node, bir LayoutNode öğesine uygulanan her Modifier.Element için bir kez oluşturulan, uzun ömürlü ve durum bilgisi olan bir nesnedir. Her geçişte oluşturma yoluyla yeniden somutlaştırılmak yerine yeniden oluşturma işleminden sonra da varlığını sürdürür. Modifier.Node'i neden ve nasıl tasarladığımız hakkında daha fazla bilgi için Compose Değiştiricileri ayrıntılı incelemesi başlıklı makaleyi inceleyin.
Bu belgede, Modifier.composed sürümünden Modifier.Node sürümüne nasıl geçiş yapılacağı açıklanmaktadır. Bu API'nin genel kullanımı hakkında daha fazla bilgi için Modifier.Node kullanarak özel değiştirici davranışı uygulama başlıklı makaleyi inceleyin.
Modifier.Node'un performans avantajları
Modifier.composed kullanmak, performansla ilgili birkaç temel darboğaza neden olur:
- Durum yönetimi ek yükü: Bu kapsamda durum yönetimi için
rememberçağrıları ve anlık görüntü durum nesneleri gerekir. Bu da slot tablosunu gereksiz oluşturma gruplarıyla doldurur ve bellek baskısını artırır. - Maliyetli yaşam döngüsü erişimi: Değiştiricinin yaşam döngüsüne erişmek için
DisposableEffectgibi efektlerin kullanılması gerekir. Bu da daha basit kullanım alanları için gereken iş yükünü hızla artırır. - Atlanabilirlik eksikliği:
composedişlevine iletilen lambda birModifierdöndürdüğünden, Compose derleyicisi bunu atlanabilir olarak işaretleyemez ve düzen her yeniden oluşturulduğunda yeniden yürütülmeye zorlar. - Bozuk memoization ve eşitlik: Dış uzantı işlevinin kendisi
@Composableolmadığından derleyici, iç lambda'yı memoize edemez. Bu da her çağrıda yeni lambda tahsisatlarına neden olur. Bu ezberleme eksikliği,ComposedModifierlambda'ları referansa göre karşılaştırdığı için değiştirici eşitliğini (equals) doğrudan bozar. Bu nedenle, parametreler statik olsa bile Compose, değiştiriciyi her karede değişmiş olarak kabul eder. - Akıllı değişiklik yayılımı yok: Üst düzey composable parametre takibi olmadan, akıllı değişiklik yayılımı için yeni girişleri önceki girişlerle karşılaştırmanın bir yolu yoktur.
Genel olarak, Modifier.composed API şekli pahalı kod yazmayı teşvik eder ve Compose çalışma zamanının ek değiştirici optimizasyonları uygulamasını engeller.
Temel taşıma adımları
Aşağıdaki örnekte, Modifier.composed ile uygulanan tipik bir özel değiştirici gösterilmektedir. Daha fazla bilgi için Modifier.Node kullanarak özel değiştirici davranışı uygulama başlıklı makaleyi inceleyin.
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 ) } }
Özel bir
Modifier.Node(veyaDelegatingNode) oluşturun: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) { } }
Özel değiştiricinizin ihtiyaçlarına bağlı olarak
Modifier.Node'ın yardımcı API'lerinden birini veya daha fazlasını uygulayın (örneğin, işaretçi giriş API'lerine erişmesi gerekiyorsaPointerInputModifierNode):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() } }
Özel düğümünüzü oluşturan ve güncelleyen bir
ModifierNodeElementoluşturun: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 } }
Değiştirici fabrikasını
ModifierNodeElementöğesine yönlendirecek şekilde güncelleyin:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
Sık kullanılan taşıma tarifleri
Aşağıdaki tariflerde, yaygın kalıpların Modifier.composed'ten Modifier.Node veya @Composable değiştirici fabrikalarına nasıl taşınacağı gösterilmektedir.
CompositionLocal'a erişme
Kalıp: LocalDensity, Theme veya LocalView gibi tek bir CompositionLocal okuma.
Taşıma yolu: Değiştiriciyi @Composable ile işaretleyin. CompositionLocal değerine erişmek için composed değiştirici ve @Composable değiştirici fabrikası kullanmak arasında anlamsal bir fark vardır. @Composable fabrikası kullanıldığında CompositionLocal değerleri, değiştirici fabrikasının çağrı sitesinde çözümlenir. İstenen davranış bu değilse CompositionLocalConsumerModifierNode kullanarak CompositionLocal okuyan özel bir Modifier.Node uygulaması kullanın.
Daha fazla bilgi için Birleştirilebilir değiştirici fabrikası kullanarak özel değiştirici oluşturma başlıklı makaleyi inceleyin.
// ❌ 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()
Kalıp: Sonraki bir değiştiriciye uygulanabilecek bir CompositionLocal okuma.
Taşıma yolu: CompositionLocalConsumerModifierNode uygulayan ve tüm değiştiricilerin özelliklerini birleştiren özel bir Modifier.Node oluşturun.
// ❌ 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) } } }
Düzen olmayan bir composable işleve erişme
Kalıp: Değiştiricinin, @Composable ile ek açıklama eklenmiş ve bir nesne (ör. colorResource veya ScrollableDefaults.flingBehavior) döndüren bir işleve erişmesi gerekir.
Geçiş yolu: Değiştiriciye @Composable ile ek açıklama ekleyin.
// ❌ 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) }
Eş yordam kapsamına erişme
Model: Modifier.composed, eş yordamları başlatmak amacıyla bir coroutineScope nesnesine erişmek için rememberCoroutineScope yöntemini yürütmek üzere kullanılır.
Taşıma yolu: Değiştirici yaşam döngüsüne bağlı bir coroutineScope mülkü olan özel bir Modifier.Node kullanın (Modifier.composed içindeki rememberCoroutineScope gibi):
// ❌ 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() } }
Bir durumu hatırlama
Kalıp: Yeniden oluşturma işlemlerinde durumu kaydetmek için Modifier.composed içinde remember kullanma.
Taşıma yolu: Modifier.Node, durumu aynı şekilde tutacak şekilde oluşturulmuştur. Durum, daha net bir yaşam döngüsüne sahip diğer sınıf özellikleri gibi bir örnek içinde tutulabilir:
// ❌ 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() } }
Efekt kullanma
Kalıp: Oluşturma yaşam döngüsüne bağlı işlemleri yürütmek için efektleri kullanma (ör. Modifier.composed oluşturmaya girdiğinde veya oluşturmadan çıktığında).
Geçiş yolu: Modifier.Node, aynı işlemleri yürütmek için kullanılabilecek net yaşam döngüsü geri çağırma yöntemlerine sahiptir. Örneğin, LaunchedEffect genellikle Modifier.Node onAttach yönteminin içindeki coroutineScope kullanılarak değiştirilebilir:
// ❌ 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) } } }
Animasyon durumunu koruma
Kalıp: animate*AsState kullanarak Modifier.composed.
Taşıma yolu: animate*AsState, yaşam döngüsü geri çağırma yöntemlerini izleyen ve Animatable durumunu tutan özel bir değiştiriciye ayrılabilir:
// ❌ 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 } } } }