Modifier.composed در Compose 1.0 معرفی شد تا به شما امکان دهد از اصلاحکنندهها به عناصر
ترکیب دسترسی داشته باشید. برای مثال، یکی از موارد استفاده کلیدی ایجاد
اصلاحگر حالتداری است که وضعیت محلی را بهیاد میآورد و آن را با دیگر
اصلاحگرها در کارخانه 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 ) }
بهجای Modifier.composed از Modifier.Node استفاده کنید، زیرا
Modifier.Node نحوه مدیریت وضعیت در اصلاحکنندهها را بهبود میبخشد. Modifier.Node یک شیء با وضعیت و ماندگاری طولانی است که یکبار در هر
Modifier.Element اعمالشده بر LayoutNode ایجاد میشود و بهجای اینکه در هر گذر ازطریق ترکیب دوباره مادیسازی شود،
در ترکیب مجدد باقی میماند. برای اطلاعات بیشتر درباره اینکه چرا و چگونه Modifier.Node را طراحی کردیم، به
کاوش عمیق «اصلاحکنندههای نوشتن» مراجعه کنید.
این سند نحوه انتقال از Modifier.composed به
Modifier.Node را شرح میدهد. برای کسب اطلاعات بیشتر درباره استفاده عمومی از این API، به
پیادهسازی رفتار اصلاحگر سفارشی بااستفاده از Modifier.Node مراجعه کنید.
مزایای عملکرد Modifier.Node
استفاده از Modifier.composed چندین گلوگاه عملکردی اساسی را معرفی میکند:
- سربار مدیریت حالت: مدیریت حالت در این محدوده به
rememberفراخوانی و شیء حالت لحظهای نیاز دارد که جدول جایگاه را با گروههای ترکیب غیرضروری متورم میکند و فشار حافظه را افزایش میدهد. - دسترسی به چرخه حیات پرهزینه: دسترسی به چرخه حیات اصلاحگر نیازمند استفاده از جلوههایی مثل
DisposableEffectاست که بهسرعت کار موردنیاز برای موارد استفاده سادهتر را افزایش میدهد. - عدم امکان رد کردن: چون لامبدای ارسالشده به
composedیکModifierبرمیگرداند، کامپایلر Compose نمیتواند آن را بهعنوان قابل رد کردن علامتگذاری کند و مجبور میشود هر زمان چیدمان بازآفرینی میشود، آن را دوباره اجرا کند. - برابری و حافظهسازی شکسته: چون تابع افزونه بیرونی
خودش
@Composableنیست، کامپایلر نمیتواند لامبدای داخلی را حافظهسازی کند، که منجر به تخصیص لامبدای تازه در هر فراخوانی میشود. این عدم حافظهسازی مستقیماً برابری اصلاحکننده (equals) را نقض میکند، زیراComposedModifierلامبداها را براساس مرجع مقایسه میکند. درنتیجه، «ترکیب» اصلاحکننده را بهعنوان تغییریافته در هر قاب درنظر میگیرد، حتی اگر پارامترها ایستا باشند. - بدون انتشار تغییر هوشمند: بدون ردیابی پارامتر ترکیبی سطح بالا، هیچ راهی برای مقایسه ورودیهای جدید با ورودیهای قبلی برای انتشار تغییر هوشمند وجود ندارد.
بهطورکلی، شکل Modifier.composed API نوشتن کد پرهزینه را تشویق میکند
و مانع از اعمال بهینهسازیهای اصلاحگر اضافی توسط زمان اجرای Compose میشود.
مراحل انتقال اصلی
مثال زیر یک اصلاحگر سفارشی معمولی را نشان میدهد که با
Modifier.composed پیادهسازی شده است. برای زمینههای بیشتر، به
پیادهسازی رفتار اصلاحکننده سفارشی بااستفاده از 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 ) } }
Modifier.Nodeسفارشی (یاDelegatingNode) ایجاد کنید: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) { } }
بسته به اینکه تغییردهنده سفارشی شما به چه چیزی نیاز دارد، یک یا چند مورد از میاناهای برنامهسازی کاربردی کمکی
Modifier.Nodeرا پیادهسازی کنید (برای مثال،PointerInputModifierNodeاگر به دسترسی به میاناهای برنامهسازی کاربردی ورودی اشارهگر نیاز دارد):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() } }
ModifierNodeElementرا ایجاد کنید که گره سفارشی شما را ایجاد و بهروزرسانی میکند: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 } }
کارخانه اصلاحگر را بهروز کنید تا به
ModifierNodeElementاشاره کند:fun Modifier.underline( color: Color, thickness: Dp = 2.dp, animationDurationMillis: Int = 300 ): Modifier = this then UnderlineElement(color, thickness, animationDurationMillis)
دستورالعملهای رایج انتقال
دستورالعملهای زیر نشان میدهد چگونه الگوهای رایج را از Modifier.composed
به Modifier.Node یا کارخانههای اصلاحکننده @Composable انتقال دهید.
دسترسی به CompositionLocal
الگو: خواندن یک CompositionLocal مثل LocalDensity،
Theme، یا LocalView.
مسیر انتقال: اصلاحگر را با @Composable علامتگذاری کنید. بین استفاده از اصلاحگر composed و کارخانه اصلاحگر @Composable برای دسترسی به CompositionLocal تفاوت معنایی وجود دارد—با کارخانه @Composable، مقادیر CompositionLocal در سایت تماس کارخانه اصلاحگر حلوفصل میشوند. اگر این رفتار موردنظر نیست، از پیادهسازی سفارشی Modifier.Node
استفاده کنید که CompositionLocal را بااستفاده از
CompositionLocalConsumerModifierNode میخواند.
برای اطلاعات بیشتر، به ایجاد اصلاحگر سفارشی بااستفاده از کارخانه اصلاحگر ترکیبی مراجعه کنید.
// ❌ 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()
الگو: خواندن CompositionLocal که ممکن است روی اصلاحکننده بعدی اعمال شود.
مسیر انتقال: Modifier.Node سفارشی ایجاد کنید که
CompositionLocalConsumerModifierNode را پیادهسازی کند و همه قابلیتهای
اصلاحکنندهها را ترکیب کند.
// ❌ 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) } } }
دسترسی به تابع ترکیبشدنی غیرچیدمانی
الگو: اصلاحکننده باید به تابعی که با
@Composable حاشیهنویسی شده است دسترسی داشته باشد و شیئی (برای مثال، colorResource یا
ScrollableDefaults.flingBehavior) برگرداند.
مسیر انتقال: اصلاحگر را با @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) }
دسترسی به حوزه روتین همکار
الگو: از Modifier.composed برای اجرای rememberCoroutineScope بهمنظور
دسترسی به شیء coroutineScope برای راهاندازی روتینهای همزمان استفاده میشود.
مسیر انتقال: از Modifier.Node سفارشی استفاده کنید که دارای coroutineScope
دارایی مرتبط با چرخه عمر اصلاحکننده (مانند rememberCoroutineScope
درون 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() } }
بهخاطر سپردن وضعیت
الگو: استفاده از remember در Modifier.composed برای ذخیره وضعیت در
ترکیبهای مجدد.
مسیر انتقال: Modifier.Node برای حفظ وضعیت به همان روش ساخته شده است. وضعیت میتواند درون نمونهای نگهداری شود، درست مانند هر دارایی کلاس دیگری
با چرخه حیات واضحتر:
// ❌ 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() } }
استفاده از جلوه
الگو: استفاده از جلوهها برای اجرای عملیاتهای مرتبط با چرخه حیات ترکیب (برای مثال، وقتی Modifier.composed وارد ترکیب یا از آن خارج شد).
مسیر انتقال: Modifier.Node دارای برگشتهای تماس چرخه حیات واضحی است که میتوان از آنها برای اجرای عملیات مشابه استفاده کرد. برای مثال، LaunchedEffect را میتوان
معمولاً بااستفاده از coroutineScope در روش
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) } } }
نگهداشتن وضعیت پویانمایی
الگو: Modifier.composed بااستفاده از animate*AsState.
مسیر انتقال: animate*AsState را میتوان به یک تغییردهنده سفارشی تجزیه کرد که
بازخوانهای چرخه حیات را پایش میکند و وضعیت 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 } } } }