बैकवर्ड राइट

कंपोज़, फ़्रेम को तीन चरणों में पूरा करता है. ये चरण, तय किए गए क्रम में होते हैं:

तीन चरण: 1. कंपोज़िशन, 2. लेआउट, 3. ड्रॉइंग
पहली इमेज. Compose फ़्रेम के तीन चरण
  1. कंपोज़िशन: यूज़र इंटरफ़ेस (यूआई) ट्री बनाने और उसे अपडेट करने के लिए, @Composable फ़ंक्शन चलाता है.
  2. लेआउट: यह बच्चों की पहचान करता है और फिर उन्हें जगह देता है.
  3. ड्रॉइंग: यह कैनवस पर ड्रॉ करने के निर्देश देता है, ताकि स्क्रीन पर पिक्सल रेंडर किए जा सकें.

किसी भी फ़ेज़ के दौरान, जब 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
}


बैकवर्ड राइटिंग को रोकने के लिए मुख्य नियम

  1. अगर कंपोज़िशन में onGloballyPositioned, onSizeChanged या LayoutModifier की स्थिति को पढ़ा जाता है, तो उसे न लिखें. ऐसा करने से, पहले फ़्रेम के सही होने से जुड़ी समस्या होती है.
    • अगर लेआउट के कोऑर्डिनेट या साइज़ सिर्फ़ कस्टम ड्राइंग के लिए ज़रूरी हैं, तो उन्हें सीधे तौर पर ड्रॉ या लेआउट फ़ेज़ में पढ़ें. उदाहरण के लिए, Modifier.drawWithCache या Modifier.layout का इस्तेमाल करके.
    • विंडो-लेवल के साइज़ के लिए (WindowWidthSizeClass): विंडो लेवल पर, होइस्ट के साइज़ का आकलन करें. लोकल मेज़रमेंट होने से पहले, कंपोज़िशन, विंडो के साइज़ के हिसाब से क्लास में बंट जाती है.
    • कंपोज़िशन को एक जैसा रखना: एक ही कस्टम लेआउट या FlowRow या LazyVerticalGrid जैसे कॉम्पोनेंट का इस्तेमाल करें. ये कॉम्पोनेंट, कंपोज़िशन में इस्तेमाल की गई स्थिति को बदले बिना या उसे फिर से कंपोज़ किए बिना, दूसरे फ़ेज़ के दौरान मेज़रमेंट और प्लेसमेंट को अडजस्ट करते हैं.
    • सब-कंपोज़िशन का इस्तेमाल करें: जब चाइल्ड कंपोज़ेबल को लोकल चौड़ाई या ऊंचाई के आधार पर ब्रांच करना हो, तब BoxWithConstraints या SubcomposeLayout का इस्तेमाल करें. सावधानी बरतें: सबकंपोज़िशन से परफ़ॉर्मेंस पर असर पड़ता है. आम तौर पर, इससे बचा जा सकता है.
    • आखिरी विकल्प के तौर पर: पहले फ़्रेम को गलत होने दें. साथ ही, साइज़ को onSizeChanged में सेव करें, ताकि दूसरे फ़्रेम को फिर से कंपोज़ किया जा सके. इस वजह से, लेआउट में बदलाव दिखता है, परफ़ॉर्मेंस में रुकावट आती है, और इनफ़िनिट लूप का खतरा होता है.
  2. कंपोज़िशन में पहली बार पढ़ने के बाद, स्थिति में बदलाव न करें:
    • हालांकि, कंपोज़िशन के दौरान MutableState ऑब्जेक्ट में सुरक्षित तरीके से लिखा जा सकता है, लेकिन यह पक्का करें कि आपने कंपोज़िशन में पहले से पढ़े गए किसी स्टेट में न लिखा हो.SideEffect हमारा सुझाव है कि कंपोज़िशन में स्टेट लिखने की ज़रूरत पड़ने पर, rememberUpdatedState का इस्तेमाल करें. कंपोज़िशन के दौरान किसी स्टेट में किसी दूसरे तरीके से लिखना, आम तौर पर किसी इफ़ेक्ट के मौजूद न होने या गलत तरीके से डिज़ाइन की गई स्टेट या कंपोज़ेबल का संकेत होता है. ध्यान रखें कि कंपोज़िशन, ऑप्टिमिस्टिक होती है और हमेशा किसी स्टेट की नई वैल्यू के साथ काम करती है. इसलिए, हो सकता है कि आपको रीकंपोज़िशन में, स्टेट में हुए सभी बदलाव न दिखें. यूज़र इंटरफ़ेस (यूआई) अपडेट का इस्तेमाल, एक बार होने वाले इवेंट को हैंडल करने के तरीके के तौर पर नहीं किया जाना चाहिए. इससे, कंपोज़िशन फिर से होने पर, किसी स्टेट की वैल्यू को अपडेट करना असामान्य हो जाता है.
    • कंपोज़िशन के बाहर देखी गई स्थितियों में बदलाव न करें. उदाहरण के लिए, ViewModel फ़ील्ड या isVisible फ़्लैग. स्टेट में बदलाव करने वाले कोड को इवेंट लैम्डा (onClick), कोरुटीन (LaunchedEffect) या साइड इफ़ेक्ट (SideEffect) में लिखा जाना चाहिए. rememberUpdatedState एक अपवाद है, क्योंकि इसे ऐसे स्टेट में बदलाव करने के लिए डिज़ाइन किया गया है जिसका इस्तेमाल सिर्फ़ @Composable बॉडी में किया जाता है.
  3. स्टेट को पढ़ने की प्रोसेस को आखिरी चरण तक के लिए टालना:
    • Draw (Modifier.graphicsLayer { alpha = ... }) या लेआउट (Modifier.offset { IntOffset(...) }) में पढ़ने की स्थितियां यह पक्का करती हैं कि बदलावों से सिर्फ़ दूसरे या तीसरे चरण की पुष्टि रद्द हो. इससे पहले चरण (कंपोज़िशन) को पूरी तरह से छोड़ दिया जाता है.