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 को क्यों और कैसे डिज़ाइन किया, इस बारे में ज़्यादा जानने के लिए, Compose Modifiers के बारे में ज़्यादा जानकारी लेख पढ़ें.
इस दस्तावेज़ में, Modifier.composed से Modifier.Node पर माइग्रेट करने का तरीका बताया गया है. इस एपीआई के सामान्य इस्तेमाल के बारे में ज़्यादा जानने के लिए, Modifier.Node का इस्तेमाल करके, कस्टम मॉडिफ़ायर के व्यवहार को लागू करना लेख पढ़ें.
Modifier.Node के परफ़ॉर्मेंस से जुड़े फ़ायदे
Modifier.composed का इस्तेमाल करने से, परफ़ॉर्मेंस से जुड़ी कई बुनियादी समस्याएं आती हैं:
- स्टेट मैनेजमेंट का ओवरहेड: इस स्कोप में स्टेट को मैनेज करने के लिए,
rememberकॉल और स्नैपशॉट स्टेट ऑब्जेक्ट की ज़रूरत होती है. इससे स्लॉट टेबल में गैर-ज़रूरी कंपोज़िशन ग्रुप बढ़ जाते हैं और मेमोरी पर दबाव बढ़ जाता है. - लाइफ़साइकल का महंगा ऐक्सेस: मॉडिफ़ायर के लाइफ़साइकल को ऐक्सेस करने के लिए,
DisposableEffectजैसे इफ़ेक्ट का इस्तेमाल करना ज़रूरी है. इससे, इस्तेमाल के आसान उदाहरणों के लिए ज़रूरी काम तेज़ी से बढ़ जाता है. - स्किप करने की सुविधा उपलब्ध नहीं है:
composedको पास किया गया लैम्डा,Modifierदिखाता है. इसलिए, Compose कंपाइलर इसे स्किप करने की सुविधा के तौर पर मार्क नहीं कर सकता. इस वजह से, लेआउट के फिर से कंपोज़ होने पर, इसे फिर से एक्ज़ीक्यूट करना पड़ता है. - मेमोइज़ेशन और समानता की समस्या: आउटर एक्सटेंशन फ़ंक्शन खुद
@Composableनहीं है. इसलिए, कंपाइलर इनर लैम्डा को मेमोइज़ नहीं कर सकता. इस वजह से, हर कॉल पर नए लैम्डा असाइन किए जाते हैं. मेमोइज़ेशन की कमी की वजह से, मॉडिफ़ायर की समानता (equals) सीधे तौर पर टूट जाती है. ऐसा इसलिए होता है, क्योंकिComposedModifier, रेफ़रंस के हिसाब से लैम्ब्डा की तुलना करता है. इसलिए, 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 को पढ़ना, जैसे कि LocalDensity, Theme या LocalView.
माइग्रेशन का तरीका: मॉडिफ़ायर को @Composable से मार्क करें. composed मॉडिफ़ायर और @Composable मॉडिफ़ायर फ़ैक्ट्री का इस्तेमाल करके CompositionLocal को ऐक्सेस करने के बीच सिमैंटिक अंतर होता है. @Composable फ़ैक्ट्री के साथ, CompositionLocal वैल्यू को मॉडिफ़ायर फ़ैक्ट्री की कॉल साइट पर हल किया जाता है. अगर आपको यह तरीका इस्तेमाल नहीं करना है, तो Modifier.Node को कस्टम तरीके से लागू करें. इससे CompositionLocalConsumerModifierNode का इस्तेमाल करके CompositionLocal को पढ़ा जा सकेगा.
ज़्यादा जानकारी के लिए, कंपोज़ेबल मॉडिफ़ायर फ़ैक्ट्री का इस्तेमाल करके कस्टम मॉडिफ़ायर बनाना लेख पढ़ें.
// ❌ 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() } }
किसी स्थिति को याद रखना
पैटर्न: Modifier.composed में remember का इस्तेमाल करके, फिर से कंपोज़ करने पर भी स्थिति को सेव किया जाता है.
माइग्रेशन पाथ: 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 को आम तौर पर Modifier.Node onAttach तरीके के अंदर coroutineScope का इस्तेमाल करके बदला जा सकता है:
// ❌ 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) } } }
ऐनिमेशन की स्थिति को बनाए रखना
पैटर्न: animate*AsState का इस्तेमाल करके Modifier.composed.
माइग्रेशन का तरीका: 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 } } } }