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.Node از Modifier.composed استفاده کنید، زیرا Modifier.Node نحوه مدیریت حالت در درون modifierها را بهبود میبخشد. یک Modifier.Node یک شیء با طول عمر بالا و حالتپذیر است که یک بار به ازای هر Modifier.Element اعمال شده بر روی یک LayoutNode ایجاد میشود و به جای اینکه در هر بار ترکیببندی دوباره ساخته شود، از ترکیببندی مجدد جان سالم به در میبرد. برای اطلاعات بیشتر در مورد اینکه چرا و چگونه Modifier.Node را طراحی کردهایم، به بخش Compose Modifiers deep diver مراجعه کنید.
این سند نحوه مهاجرت از Modifier.composed به Modifier.Node را شرح میدهد. برای اطلاعات بیشتر در مورد کاربرد عمومی این API، به بخش «پیادهسازی رفتار اصلاحکننده سفارشی با استفاده از Modifier.Node» مراجعه کنید.
مزایای عملکرد Modifier.Node
استفاده از Modifier.composed چندین گلوگاه عملکردی اساسی ایجاد میکند:
- سربار مدیریت وضعیت: مدیریت وضعیت در این حوزه نیازمند فراخوانیهای
rememberو گرفتن عکسهای فوری از اشیاء وضعیت است که جدول اسلات را با گروههای ترکیبی غیرضروری پر میکند و فشار حافظه را افزایش میدهد. - دسترسی پرهزینه به چرخه عمر: دسترسی به چرخه عمر اصلاحکننده نیاز به استفاده از افکتهایی مانند
DisposableEffectدارد که به سرعت کار مورد نیاز برای موارد استفاده سادهتر را افزایش میدهد. - عدم قابلیت رد شدن: از آنجا که لامبدا ارسالی به
composedیکModifierبرمیگرداند، کامپایلر Compose نمیتواند آن را به عنوان قابل رد شدن علامتگذاری کند و هر زمان که layout دوباره ترکیب میشود، مجبور به اجرای مجدد آن میشود. - اختلال در Memoize کردن و برابری: از آنجایی که خود تابع افزونه بیرونی یک
@Composableنیست، کامپایلر نمیتواند لامبدا داخلی را Memoize کند، که منجر به تخصیصهای لامبدا جدید در هر فراخوانی میشود. این عدم Memoize کردن مستقیماً برابری اصلاحکننده (equals) را از بین میبرد، زیراComposedModifierلامبداها را با ارجاع مقایسه میکند. در نتیجه، Compose اصلاحکننده را در هر فریم تغییر یافته در نظر میگیرد، حتی زمانی که پارامترها استاتیک باشند. - عدم انتشار هوشمند تغییرات: بدون ردیابی پارامترهای قابل ترکیب در سطح بالا، هیچ راهی برای تمایز ورودیهای جدید در برابر ورودیهای قبلی برای انتشار هوشمند تغییرات وجود ندارد.
در مجموع، شکل API Modifier.composed نوشتن کدهای پرهزینه را تشویق میکند و مانع از اعمال بهینهسازیهای اضافی برای اصلاحکنندهها توسط زمان اجرای 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) { } }
بسته به نیاز اصلاحکننده سفارشی خود، یک یا چند API کمکی از
Modifier.Nodeپیادهسازی کنید (برای مثال،PointerInputModifierNodeاگر نیاز به دسترسی به APIهای ورودی اشارهگر دارد):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 است که به چرخه حیات modifier گره خورده است (مانند 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 برای نگهداری state به همین روش ساخته شده است. state را میتوان درون یک نمونه نگهداری کرد، درست مانند هر ویژگی کلاس دیگر با چرخه حیات واضحتر:
// ❌ 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 } } } }