बैकवर्ड राइट

Compose, फ़्रेम को तीन चरणों में लागू करता है. ये चरण एक के बाद एक, क्रम से पूरे होते हैं:

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

जब भी किसी फ़ेज़ के दौरान Compose State को रीड किया जाता है, तो Compose उस स्टेट और उससे जुड़े फ़ेज़ के बीच की डिपेंडेंसी को अपने-आप रिकॉर्ड कर लेता है.


"उल्टा" लिखने की वजह क्या है?

बैकवर्ड राइट तब होता है, जब किसी स्टेट को बाद के फ़ेज़ में बदला जाता है (या डाउनस्ट्रीम स्कोप में—इसका मतलब है कि कंपोज़ेबल स्कोप को उसी कंपोज़िशन पास में बाद में लागू किया जाता है) जहां उसे पढ़ा गया था. इससे Compose को फिर से कंपोज़ करने के लिए मजबूर होना पड़ता है. इसके लिए, पहले के फ़ेज़ या कंपोज़ेबल को फिर से चलाने के लिए शेड्यूल किया जाता है. बैकवर्ड राइट, एक ऐसा रीकंपोज़िशन लूप है जिसे ऑप्टिमाइज़ नहीं किया गया है.

रीकंपोज़िशन लूप के फ़ेज़ के दौरान, बैकवर्ड राइट का उदाहरण
दूसरी इमेज. रीकंपोज़िशन लूप के फ़ेज़ के ज़रिए, बैकवर्ड राइट का उदाहरण

बैकवर्ड राइट के नतीजे

बैकवर्ड राइट्स ज़रूरी नहीं कि खराब हों और इनसे हमेशा क्रैश ट्रिगर नहीं होते. हालांकि, ये असरदार नहीं होते और इनसे ऐप्लिकेशन की परफ़ॉर्मेंस को कई तरीकों से नुकसान पहुंच सकता है:

  • अतिरिक्त फ़्रेम रेंडरिंग और छोड़े गए फ़्रेम: बैकवर्ड राइट की वजह से, Compose को लगातार फ़्रेम में ग़ैर-ज़रूरी कंपोज़िशन पास लागू करने पड़ते हैं. इससे सीपीयू और जीपीयू के संसाधन बर्बाद होते हैं और जैंक की समस्या हो सकती है.
  • पहले फ़्रेम के सही होने से जुड़ी समस्याएं: अगर आपके कॉम्पोनेंट को अपने फ़ाइनल डाइमेंशन या स्थिति को ठीक करने के लिए, बैकवर्ड राइट की ज़रूरत है, तो पहला फ़्रेम अमान्य, डिफ़ॉल्ट या सेट नहीं किए गए डेटा (जैसे, शून्य साइज़ या गलत ऑफ़सेट) के साथ रेंडर होता है. इस वजह से, दूसरा फ़्रेम रेंडर होने पर, विज़ुअल पॉपिंग या लेआउट फ़्लैशिंग दिखती है.
  • इनफ़ाइनाइट रीकंपोज़िशन लूप: अगर स्टेट में बदलाव होने से लेआउट का साइज़ बदलता है और लेआउट का साइज़ लगातार स्टेट में नई वैल्यू लिखता है, तो आपको इनफ़ाइनाइट फ़्रेम लूप बनाने का जोखिम हो सकता है. इसमें स्क्रीन, हर फ़्रेम को लगातार रीकंपोज़ करती है और कभी भी स्थिर नहीं होती.

अलग-अलग चरणों में आगे की ओर फ़्लो

स्टेटस में बदलाव हमेशा इन चरणों के हिसाब से आगे की ओर बढ़ना चाहिए:

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