تم طرح Modifier.composed في الإصدار 1.0 من Compose للسماح لك بالوصول إلى عناصر التركيب من المعدِّلات. على سبيل المثال، إحدى حالات الاستخدام الرئيسية هي إنشاء معدِّل ذي حالة يتذكّر الحالة المحلية ويشاركها مع المعدِّلات الأخرى في مصنع 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.Node هو عنصر دائم ومحتفظ بالحالة يتم إنشاؤه مرة واحدة لكل Modifier.Element يتم تطبيقه على LayoutNode، ويستمر بعد إعادة التركيب بدلاً من إعادة إنشائه من خلال التركيب في كل عملية. لمزيد من المعلومات حول سبب تصميم Modifier.Node وطريقة تصميمه، يُرجى الاطّلاع على نظرة تفصيلية حول معدِّلات Compose.
يوضّح هذا المستند كيفية نقل البيانات من Modifier.composed إلى Modifier.Node. لمزيد من المعلومات حول الاستخدام العام لواجهة برمجة التطبيقات هذه، يُرجى الاطّلاع على
تنفيذ سلوك معدِّل مخصّص باستخدام Modifier.Node.
مزايا الأداء في Modifier.Node
يؤدي استخدام Modifier.composed إلى حدوث العديد من الاختناقات الأساسية في الأداء:
- تكلفة إدارة الحالة: تتطلّب إدارة الحالة في هذا النطاق
rememberعمليات استدعاء وعناصر حالة لقطة، ما يؤدي إلى تضخيم جدول الخانات بمجموعات تركيب غير ضرورية وزيادة الضغط على الذاكرة. - الوصول إلى دورة الحياة باهظ التكلفة: يتطلّب الوصول إلى دورة حياة المعدِّل استخدام تأثيرات مثل
DisposableEffect، ما يزيد بسرعة من العمل المطلوب لحالات الاستخدام الأبسط. - عدم إمكانية التخطّي: بما أنّ lambda التي تم تمريرها إلى
composedتعرضModifier، لا يمكن لمترجم Compose تصنيفها على أنّها قابلة للتخطّي، ما يجبرها على إعادة التنفيذ كلما تمت إعادة إنشاء التنسيق. - تعطُّل التخزين المؤقت والمساواة: بما أنّ دالة الإضافة الخارجية ليست
@Composable، لا يمكن للمترجم تخزين تعبير lambda الداخلي مؤقتًا، ما يؤدي إلى تخصيص تعبيرات lambda جديدة في كل طلب. يؤدي عدم استخدام التخزين المؤقت إلى إيقاف عمل وظيفة التحقّق من تطابق المعدِّلات (equals) بشكل مباشر، لأنّComposedModifierتقارن تعبيرات lambda حسب المرجع. نتيجةً لذلك، يتعامل Compose مع المعدِّل على أنّه تغيّر في كل إطار حتى عندما تكون المَعلمات ثابتة. - عدم إتاحة نقل التغييرات الذكي: بدون تتبُّع مَعلمات الدوال المركّبة على أعلى مستوى، لا يمكن مقارنة الإدخالات الجديدة بالإدخالات السابقة لنقل التغييرات الذكي.
بشكل عام، يشجّع شكل واجهة برمجة التطبيقات 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) { } }
نفِّذ واحدة أو أكثر من واجهات برمجة التطبيقات المساعدة في
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 } } } }