कंपोज़, फ़्रेम को तीन चरणों में पूरा करता है. ये चरण, तय किए गए क्रम में होते हैं:
- कंपोज़िशन: यूज़र इंटरफ़ेस (यूआई) ट्री बनाने और उसे अपडेट करने के लिए,
@Composableफ़ंक्शन चलाता है. - लेआउट: यह बच्चों की पहचान करता है और फिर उन्हें जगह देता है.
- ड्रॉइंग: यह कैनवस पर ड्रॉ करने के निर्देश देता है, ताकि स्क्रीन पर पिक्सल रेंडर किए जा सकें.
किसी भी फ़ेज़ के दौरान, जब Compose State को रीड किया जाता है, तो Compose उस स्थिति और उससे जुड़े फ़ेज़ के बीच अपने-आप डिपेंडेंसी रिकॉर्ड करता है.
किसी अक्षर को "उल्टा" कब माना जाता है?
बैकवर्ड राइट तब होता है, जब किसी स्टेट में बाद के फ़ेज़ में बदलाव किया जाता है (या डाउनस्ट्रीम स्कोप में—इसका मतलब है कि कंपोज़ेबल स्कोप को एक ही कंपोज़िशन पास में बाद में क्रम से एक्ज़ीक्यूट किया जाता है) उस जगह से जहां इसे पढ़ा गया था. इससे Compose को पहले के फ़ेज़ या कंपोज़ेबल को फिर से चलाने के लिए शेड्यूल करके, फिर से कंपोज़ करना पड़ता है. बैकवर्ड राइट, ऑप्टिमाइज़ न किया गया रीकंपोज़िशन लूप होता है.
बैकवर्ड राइट के नतीजे
बैकवर्ड राइटिंग हमेशा खराब नहीं होती. साथ ही, इससे हमेशा क्रैश ट्रिगर नहीं होते. हालांकि, यह सही तरीका नहीं है. इससे ऐप्लिकेशन की परफ़ॉर्मेंस पर कई तरह से असर पड़ सकता है:
- ज़्यादा फ़्रेम रेंडर करना और फ़्रेम छोड़ना: बैकवर्ड राइट की वजह से, Compose को लगातार फ़्रेम में रिडंडेंट कंपोज़िशन पास को एक्ज़ीक्यूट करना पड़ता है. इससे सीपीयू और जीपीयू के संसाधनों का इस्तेमाल होता है और जंक होने की समस्या हो सकती है.
- पहले फ़्रेम में गड़बड़ी होने की समस्याएं: अगर आपके कॉम्पोनेंट को अपने फ़ाइनल डाइमेंशन या स्थिति को ठीक करने के लिए, पीछे की ओर लिखना पड़ता है, तो पहला फ़्रेम अमान्य, डिफ़ॉल्ट या बिना सेटल किए गए डेटा के साथ रेंडर होता है. जैसे, शून्य साइज़ या गलत ऑफ़सेट. इस वजह से, दूसरा फ़्रेम रेंडर होने पर, लेआउट फ़्लैश होता है या विज़ुअल पॉपिंग दिखती है.
- रीकंपोज़िशन के इनफ़ाइनाइट लूप: अगर किसी स्टेट में बदलाव करने से लेआउट के साइज़ में बदलाव होता है और लेआउट का साइज़, स्टेट में लगातार नई वैल्यू लिखता है, तो आपके पास इनफ़ाइनाइट फ़्रेम लूप बनाने का जोखिम होता है. इसमें स्क्रीन, हर फ़्रेम को लगातार रीकंपोज़ करती है और कभी भी स्थिर नहीं होती.
फ़ेज़ के हिसाब से आगे बढ़ने का फ़्लो
स्टेट में होने वाले बदलाव, हमेशा इन चरणों के हिसाब से आगे बढ़ने चाहिए:
| रीड फ़ेज़ | कॉन्टेक्स्ट लिखना | स्वीकार किया जा सकता है? | क्यों |
|---|---|---|---|
लेआउट (Modifier.offset { }) |
कंपोज़िशन | हां | कंपोज़िशन, स्थिति को अपडेट करती है → लेआउट, उसे बाद में उसी फ़्रेम में पढ़ता है. इसके लिए, उसे फिर से कंपोज़ नहीं करना पड़ता. |
ड्रॉ (graphicsLayer { }, drawBehind { }) |
कंपोज़िशन | हां | कंपोज़िशन, स्थिति को अपडेट करती है → ड्रॉ, इसे आखिरी फ़ेज़ में पढ़ता है. कंपोज़िशन और लेआउट को पूरी तरह से छोड़ दिया जाता है. |
| ड्रा करें | लेआउट | हां | लेआउट, स्थिति को अपडेट करता है और ड्रॉ उसे पढ़ता है. यह मान्य फ़्लो है. |
| कंपोज़िशन | इवेंट कॉलबैक (onClick, onValueChange) से, ड्राइविंग की स्थिति में बदलाव होता है. ध्यान दें: लेआउट कॉलबैक को इवेंट के तौर पर नहीं गिना जाता. |
हां | इवेंट, कंपोज़िशन को चलाने के लिए इस्तेमाल की जाने वाली स्थिति को बदलता है. अगर इवेंट फ़्रेम से बाहर होता है (कंपोज़िशन, लेआउट या ड्रॉ में नहीं), तो यह मान्य है. |
| कंपोज़िशन | कोरूटीन (LaunchedEffect) |
हां - सावधानी के साथ | यह लाइफ़साइकल/इवेंट के जवाब में, एसिंक्रोनस तरीके से स्थिति को अपडेट करता है. इफ़ेक्ट से किए गए बदलाव मान्य हो सकते हैं, लेकिन इससे स्टेट लेयरिंग के सही तरीके से काम न करने का पता चल सकता है. जहां तक हो सके, इनका इस्तेमाल नहीं करना चाहिए. |
| प्लेसमेंट (लेआउट में) | मेज़र (लेआउट में) | हां | लेआउट में, स्थिति को अपडेट करने के बाद प्लेसमेंट में उस स्थिति को पढ़ा जा सकता है. |
| मेज़र (लेआउट में) | प्लेसमेंट (लेआउट में) | नहीं - पीछे की ओर लिखना | प्लेसमेंट में यह जानकारी लिखने से कि उसे पढ़ा गया है, फिर से मेज़रमेंट शुरू हो जाता है. |
| कंपोज़िशन | लेआउट (onSizeChanged, LayoutModifier) |
नहीं - उल्टे क्रम में लिखना | लेआउट से कंपोज़िशन अमान्य हो जाती है → फिर से कंपोज़िशन का लूप. |
| कंपोज़िशन | ड्रॉ (drawWithContent, Canvas) |
नहीं - पीछे की ओर लिखना | ड्रॉ करने पर कंपोज़िशन अमान्य हो जाती है → फिर से कंपोज़िशन का लूप शुरू हो जाता है. |
फ़ेज़ के कॉम्बिनेशन: पीछे की ओर बनाम आगे की ओर
यहां Compose में बैकवर्ड राइट के कुछ उदाहरण दिए गए हैं. साथ ही, उन्हें ठीक करने का तरीका बताया गया है.
बैकवर्ड: कंपोज़िशन में पढ़ना, लेआउट में लिखना
- क्या होता है: कंपोज़िशन,
componentHeightको पढ़कर यह तय करती है कि कौनसा यूज़र इंटरफ़ेस (यूआई) दिखाना है. फ़्रेम के बाद के हिस्से में, लेआउट फ़ेज़, व्यू को मेज़र करता है या उन्हें जगह देता है. साथ ही,componentHeightमें नई वैल्यू लिखता है. उदाहरण के लिए,onSizeChanged,onGloballyPositionedया कस्टमLayoutModifierका इस्तेमाल करके. - नतीजा: लेआउट में
componentHeightको बदलने से, कंपोज़िशन फ़ेज़ अमान्य हो जाता है. ध्यान दें कि लेआउट मेज़रमेंट पास पूरा होने के बाद,onSizeChangedरिपोर्ट का साइज़. अगर अपडेट की गई स्थिति की वैल्यू अगले पास पर स्थिर हो जाती है, तो एक अतिरिक्त फ़्रेम के बाद, फिर से कंपोज़ होने की प्रोसेस रुक सकती है. हालांकि, अगर नई वैल्यू से साइज़ में बदलाव होता रहता है, तो इससे फ़्रेम का लूप कभी खत्म नहीं होता. इसके अलावा,onGloballyPositionedलेआउट और प्लेसमेंट, दोनों के बाद चलता है. इसलिए, इसमें स्टेट राइटिंग, लगातार रीकंपोज़िशन और रीलेआउट लूप के लिए और भी ज़्यादा संवेदनशील हो जाती है. ऐसा लगातार फ़्रेम में होता है.
// ❌ BAD: Read in Composition, Written in Layout (onSizeChanged) @Composable fun BadAspectRatioImage(painter: Painter) { var calculatedHeight by remember { mutableStateOf(0.dp) } val density = LocalDensity.current // State read during COMPOSITION: Image( painter = painter, contentDescription = "Dynamic Image", modifier = Modifier .fillMaxWidth() .height(calculatedHeight) .onSizeChanged { size -> // State write during LAYOUT phase! // Triggers backwards write and recomposition pass val aspectRatio = 16f / 9f val widthDp = with(density) { size.width.toDp() } calculatedHeight = widthDp / aspectRatio } ) } // ✅ GOOD: Measure and calculate aspect ratio height in Phase 2 (Layout) without recomposition @Composable fun GoodAspectRatioImage( painter: Painter, aspectRatio: Float = 16f / 9f, modifier: Modifier = Modifier ) { Layout( content = { Image( painter = painter, contentDescription = "Dynamic Image" ) }, modifier = modifier ) { measurables, constraints -> val width = constraints.maxWidth val height = (width / aspectRatio).toInt() // Illustrative, you can use Modifier.aspectRatio() val imageConstraints = constraints.copy( minWidth = width, maxWidth = width, minHeight = height, maxHeight = height ) val placeable = measurables.first().measure(imageConstraints) layout(width, height) { placeable.placeRelative(0, 0) } } }
पिछला वर्शन: कंपोज़िशन में पढ़ना, ड्रॉइंग में लिखना
- क्या होता है: कंपोज़ेबल बॉडी (कंपोज़िशन फ़ेज़) में स्टेट को पढ़ा जाता है, लेकिन
Modifier.drawWithContent,Modifier.drawBehindयाCanvas(ड्रॉ फ़ेज़) में बदलाव किया जाता है. - नतीजा: ड्रॉ फ़ेज़ में स्थिति बदल जाती है → कंपोज़िशन अमान्य हो जाती है → कभी न खत्म होने वाला लूप.
// ❌ BAD: Read in Composition, Written in Draw () @Composable fun BadBackwardsWriteDraw() { var componentHeight by remember { mutableStateOf(0.dp) } // State read during COMPOSITION: Text( text = "Height is: $componentHeight", modifier = Modifier.drawBehind { // State write during the DRAW phase! // Invalidates Composition -> triggers recomposition loop! componentHeight = size.height.dp } ) }
पीछे की ओर: कंपोज़िशन में पढ़ना, कंपोज़िशन में लिखना (एक ही फ़ेज़ में)
- क्या होता है: कंपोज़ेबल फ़ंक्शन में
countको पढ़ना और उसे पढ़ने के बाद, दूसरे कंपोज़ेबल के कॉन्टेंट स्लॉट में सीधे तौर परcountमें बदलाव करना. - नतीजा: स्नैपशॉट सिस्टम, एक ही कंपोज़िशन पास में पढ़ने और लिखने की प्रोसेस को रिकॉर्ड करता है. इससे मौजूदा स्कोप तुरंत अमान्य हो जाता है.
// ❌ BAD: Direct write in Composable body after read @Composable fun BadCounter() { var count by remember { mutableIntStateOf(0) } Text("Count: $count") // State read in Composition Button(onClick = {}) { count++ // State write in Composition (Backwards write!) } } // Acceptable - but error-prone as someone may add a read before the write : Direct write in Composable body before read @Composable fun OkCounter() { var count by remember { mutableIntStateOf(0) } Button(onClick = {}) { count++ // State write in Composition } Text("Count: $count") // State read in Composition }
बैकवर्ड राइटिंग को रोकने के लिए मुख्य नियम
- अगर कंपोज़िशन में
onGloballyPositioned,onSizeChangedयाLayoutModifierकी स्थिति को पढ़ा जाता है, तो उसे न लिखें. ऐसा करने से, पहले फ़्रेम के सही होने से जुड़ी समस्या होती है.- अगर लेआउट के कोऑर्डिनेट या साइज़ सिर्फ़ कस्टम ड्राइंग के लिए ज़रूरी हैं, तो उन्हें सीधे तौर पर ड्रॉ या लेआउट फ़ेज़ में पढ़ें. उदाहरण के लिए,
Modifier.drawWithCacheयाModifier.layoutका इस्तेमाल करके. - विंडो-लेवल के साइज़ के लिए (
WindowWidthSizeClass): विंडो लेवल पर, होइस्ट के साइज़ का आकलन करें. लोकल मेज़रमेंट होने से पहले, कंपोज़िशन, विंडो के साइज़ के हिसाब से क्लास में बंट जाती है. - कंपोज़िशन को एक जैसा रखना: एक ही कस्टम लेआउट या
FlowRowयाLazyVerticalGridजैसे कॉम्पोनेंट का इस्तेमाल करें. ये कॉम्पोनेंट, कंपोज़िशन में इस्तेमाल की गई स्थिति को बदले बिना या उसे फिर से कंपोज़ किए बिना, दूसरे फ़ेज़ के दौरान मेज़रमेंट और प्लेसमेंट को अडजस्ट करते हैं. - सब-कंपोज़िशन का इस्तेमाल करें: जब चाइल्ड कंपोज़ेबल को लोकल चौड़ाई या ऊंचाई के आधार पर ब्रांच करना हो, तब
BoxWithConstraintsयाSubcomposeLayoutका इस्तेमाल करें. सावधानी बरतें: सबकंपोज़िशन से परफ़ॉर्मेंस पर असर पड़ता है. आम तौर पर, इससे बचा जा सकता है. - आखिरी विकल्प के तौर पर: पहले फ़्रेम को गलत होने दें. साथ ही, साइज़ को
onSizeChangedमें सेव करें, ताकि दूसरे फ़्रेम को फिर से कंपोज़ किया जा सके. इस वजह से, लेआउट में बदलाव दिखता है, परफ़ॉर्मेंस में रुकावट आती है, और इनफ़िनिट लूप का खतरा होता है.
- अगर लेआउट के कोऑर्डिनेट या साइज़ सिर्फ़ कस्टम ड्राइंग के लिए ज़रूरी हैं, तो उन्हें सीधे तौर पर ड्रॉ या लेआउट फ़ेज़ में पढ़ें. उदाहरण के लिए,
- कंपोज़िशन में पहली बार पढ़ने के बाद, स्थिति में बदलाव न करें:
- हालांकि, कंपोज़िशन के दौरान
MutableStateऑब्जेक्ट में सुरक्षित तरीके से लिखा जा सकता है, लेकिन यह पक्का करें कि आपने कंपोज़िशन में पहले से पढ़े गए किसी स्टेट में न लिखा हो.SideEffectहमारा सुझाव है कि कंपोज़िशन में स्टेट लिखने की ज़रूरत पड़ने पर,rememberUpdatedStateका इस्तेमाल करें. कंपोज़िशन के दौरान किसी स्टेट में किसी दूसरे तरीके से लिखना, आम तौर पर किसी इफ़ेक्ट के मौजूद न होने या गलत तरीके से डिज़ाइन की गई स्टेट या कंपोज़ेबल का संकेत होता है. ध्यान रखें कि कंपोज़िशन, ऑप्टिमिस्टिक होती है और हमेशा किसी स्टेट की नई वैल्यू के साथ काम करती है. इसलिए, हो सकता है कि आपको रीकंपोज़िशन में, स्टेट में हुए सभी बदलाव न दिखें. यूज़र इंटरफ़ेस (यूआई) अपडेट का इस्तेमाल, एक बार होने वाले इवेंट को हैंडल करने के तरीके के तौर पर नहीं किया जाना चाहिए. इससे, कंपोज़िशन फिर से होने पर, किसी स्टेट की वैल्यू को अपडेट करना असामान्य हो जाता है. - कंपोज़िशन के बाहर देखी गई स्थितियों में बदलाव न करें. उदाहरण के लिए,
ViewModelफ़ील्ड याisVisibleफ़्लैग. स्टेट में बदलाव करने वाले कोड को इवेंट लैम्डा (onClick), कोरुटीन (LaunchedEffect) या साइड इफ़ेक्ट (SideEffect) में लिखा जाना चाहिए.rememberUpdatedStateएक अपवाद है, क्योंकि इसे ऐसे स्टेट में बदलाव करने के लिए डिज़ाइन किया गया है जिसका इस्तेमाल सिर्फ़@Composableबॉडी में किया जाता है.
- हालांकि, कंपोज़िशन के दौरान
- स्टेट को पढ़ने की प्रोसेस को आखिरी चरण तक के लिए टालना:
- Draw (
Modifier.graphicsLayer { alpha = ... }) या लेआउट (Modifier.offset { IntOffset(...) }) में पढ़ने की स्थितियां यह पक्का करती हैं कि बदलावों से सिर्फ़ दूसरे या तीसरे चरण की पुष्टि रद्द हो. इससे पहले चरण (कंपोज़िशन) को पूरी तरह से छोड़ दिया जाता है.
- Draw (