Jetpack Compose के चरण

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

कॉम्पोज़िशन के बारे में Compose के सभी दस्तावेज़ों में बताया गया है. इनमें Compose के बारे में सोचना और स्टेट और Jetpack Compose शामिल हैं.

फ़्रेम के तीन चरण

Compose के तीन मुख्य चरण होते हैं:

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

आम तौर पर, इन चरणों का क्रम एक जैसा होता है. इससे डेटा को एक दिशा में, कॉम्पोज़िशन से लेआउट और फिर ड्रॉइंग में भेजा जा सकता है, ताकि फ़्रेम बनाया जा सके. इसे एकतरफ़ा डेटा फ़्लो भी कहा जाता है. BoxWithConstraints और LazyColumn और LazyRow, ऐसे उदाहरण हैं जिनमें पैरंट लेआउट के फ़ेज़ के हिसाब से, चाइल्ड लेआउट तय होता है.

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

चरणों को समझना

इस सेक्शन में, कॉम्पोज़ेबल के लिए Compose के तीन चरणों को ज़्यादा जानकारी के साथ लागू करने का तरीका बताया गया है.

कंपोज़िशन

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

दूसरी इमेज. कॉम्पोज़िशन फ़ेज़ में बनाए गए आपके यूज़र इंटरफ़ेस (यूआई) को दिखाने वाला ट्री.

कोड और यूज़र इंटरफ़ेस (यूआई) ट्री का एक सबसेक्शन ऐसा दिखता है:

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

इन उदाहरणों में, कोड में मौजूद हर कॉम्पोज़ेबल फ़ंक्शन, यूज़र इंटरफ़ेस (यूआई) ट्री में मौजूद एक लेआउट नोड से मैप होता है. ज़्यादा जटिल उदाहरणों में, कॉम्पोज़ेबल में लॉजिक और कंट्रोल फ़्लो हो सकता है. साथ ही, अलग-अलग स्थितियों के हिसाब से अलग-अलग ट्री जनरेट कर सकता है.

लेआउट

लेआउट फ़ेज़ में, Compose, कॉम्पोज़िशन फ़ेज़ में बनाए गए यूआई ट्री का इस्तेमाल इनपुट के तौर पर करता है. लेआउट नोड के कलेक्शन में, 2D स्पेस में हर नोड के साइज़ और जगह तय करने के लिए ज़रूरी सारी जानकारी होती है.

चौथी इमेज. लेआउट फ़ेज़ के दौरान, यूज़र इंटरफ़ेस (यूआई) ट्री में हर लेआउट नोड का मेज़रमेंट और प्लेसमेंट.

लेआउट फ़ेज़ के दौरान, ट्री को तीन चरणों वाले एल्गोरिदम का इस्तेमाल करके ट्रैवर्स किया जाता है:

  1. बच्चों को मेज़र करना: अगर कोई नोड मौजूद है, तो वह अपने बच्चों को मेज़र करता है.
  2. अपने साइज़ का फ़ैसला खुद करना: इन मेज़रमेंट के आधार पर, कोई नोड अपने साइज़ का फ़ैसला खुद करता है.
  3. चाइल्ड नोड डालना: हर चाइल्ड नोड को नोड की अपनी जगह के हिसाब से रखा जाता है.

इस चरण के आखिर में, हर लेआउट नोड में ये चीज़ें होती हैं:

  • असाइन की गई चौड़ाई और ऊंचाई
  • x, y कॉर्डिनेट, जहां इसे ड्रॉ किया जाना चाहिए

पिछले सेक्शन में दिए गए यूज़र इंटरफ़ेस ट्री को याद करें:

पांच कॉम्पोज़ेबल और उनसे बना यूज़र इंटरफ़ेस (यूआई) ट्री वाला कोड स्निपेट. इसमें, पैरंट नोड से चाइल्ड नोड की शाखाएं निकली हुई हैं

इस ट्री के लिए, एल्गोरिदम इस तरह काम करता है:

  1. Row, अपने चाइल्ड एलिमेंट Image और Column को मेज़र करता है.
  2. Image को मेज़र किया जाता है. इसमें कोई चाइल्ड एलिमेंट नहीं है. इसलिए, यह अपने साइज़ का फ़ैसला खुद करता है और साइज़ की जानकारी Row को देता है.
  3. इसके बाद, Column को मेज़र किया जाता है. यह सबसे पहले अपने चाइल्ड (दो Text composables) को मेज़र करता है.
  4. पहले Text को मेज़र किया जाता है. इसमें कोई बच्चा नहीं है, इसलिए यह अपने साइज़ का फ़ैसला खुद लेता है और Column को अपने साइज़ की जानकारी देता है.
    1. दूसरा Text मेज़र किया जाता है. इसमें कोई चाइल्ड टैग नहीं है, इसलिए यह अपने साइज़ का फ़ैसला खुद करता है और Column को इसकी रिपोर्ट देता है.
  5. Column, बच्चे की मेज़रमेंट का इस्तेमाल करके अपना साइज़ तय करता है. यह चाइल्ड की ज़्यादा से ज़्यादा चौड़ाई और चाइल्ड की ऊंचाई के योग का इस्तेमाल करता है.
  6. Column अपने चाइल्ड विजेट को खुद के हिसाब से रखता है. साथ ही, उन्हें एक-दूसरे के नीचे वर्टिकल तौर पर रखता है.
  7. Row, बच्चे की मेज़रमेंट का इस्तेमाल करके अपना साइज़ तय करता है. यह चाइल्ड एलिमेंट की सबसे ज़्यादा ऊंचाई और उसके चाइल्ड एलिमेंट की चौड़ाई के योग का इस्तेमाल करता है. इसके बाद, यह अपने बच्चों को

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

ड्रॉइंग

ड्रॉइंग फ़ेज़ में, ट्री को फिर से ऊपर से नीचे तक ट्रैवर्स किया जाता है और हर नोड, स्क्रीन पर अपने-आप ड्रॉ होता है.

पांचवीं इमेज. ड्रॉइंग फ़ेज़ में, स्क्रीन पर पिक्सल बनाए जाते हैं.

पिछले उदाहरण का इस्तेमाल करके, ट्री कॉन्टेंट को इस तरह से बनाया गया है:

  1. Row, बैकग्राउंड का रंग जैसा कोई भी कॉन्टेंट दिखाता है.
  2. Image अपने-आप बन जाता है.
  3. Column अपने-आप बन जाता है.
  4. पहला और दूसरा Text, खुद को ड्रॉ करते हैं.

छठी इमेज. यूज़र इंटरफ़ेस (यूआई) ट्री और उसका ड्रॉ किया गया वर्शन.

पढ़े जाने की स्थिति

ऊपर बताए गए किसी एक चरण के दौरान, स्नैपशॉट स्टेटस की वैल्यू पढ़ने पर, Compose अपने-आप ट्रैक करता है कि वैल्यू पढ़ने के दौरान वह क्या कर रहा था. इस ट्रैकिंग की मदद से, Compose, स्टेट वैल्यू में बदलाव होने पर, रीडर को फिर से चला सकता है. साथ ही, यह Compose में स्टेट को मॉनिटर करने का आधार है.

स्टेटस को आम तौर पर mutableStateOf() का इस्तेमाल करके बनाया जाता है. इसके बाद, इसे दो तरीकों से ऐक्सेस किया जाता है: सीधे तौर पर value प्रॉपर्टी को ऐक्सेस करके या Kotlin प्रॉपर्टी डेलीगेट का इस्तेमाल करके. इनके बारे में ज़्यादा जानने के लिए, कंपोज़ेबल में मौजूद स्टेटस लेख पढ़ें. इस गाइड के लिए, "स्टेटस पढ़ना" का मतलब, ऐक्सेस करने के इनमें से किसी भी तरीके से है.

// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)

property delegate के तहत, "getter" और "setter" फ़ंक्शन का इस्तेमाल, स्टेट के value को ऐक्सेस और अपडेट करने के लिए किया जाता है. ये गेट्टर और सेटर फ़ंक्शन सिर्फ़ तब ट्रिगर होते हैं, जब प्रॉपर्टी को वैल्यू के तौर पर रेफ़र किया जाता है. प्रॉपर्टी बनने पर ये फ़ंक्शन ट्रिगर नहीं होते. इसलिए, ऊपर बताए गए दोनों तरीके एक जैसे हैं.

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

अलग-अलग चरणों में स्थिति पढ़ना

जैसा कि ऊपर बताया गया है, Compose में तीन मुख्य चरण होते हैं. साथ ही, Compose यह ट्रैक करता है कि इनमें से हर चरण में कौनसी स्थिति पढ़ी गई है. इससे Compose, सिर्फ़ उन खास चरणों को सूचना दे पाता है जिन्हें आपके यूज़र इंटरफ़ेस (यूआई) के उन सभी एलिमेंट के लिए काम करना होता है जिन पर असर पड़ा है.

आइए, हर चरण के बारे में जानें और यह भी जानें कि किसी स्टेटस वैल्यू को पढ़ने पर क्या होता है.

पहला चरण: कंपोज़िशन

@Composable फ़ंक्शन या लैम्ब्डा ब्लॉक में स्टेटस पढ़ने से, कॉम्पोज़िशन और संभावित तौर पर बाद के चरणों पर असर पड़ता है. जब स्टेटस वैल्यू बदलती है, तो फिर से कंपोज़र उन सभी फ़ंक्शन को फिर से चलाने का शेड्यूल करता है जो उस स्टेटस वैल्यू को पढ़ते हैं. ध्यान दें कि अगर इनपुट में कोई बदलाव नहीं हुआ है, तो रनटाइम कुछ या सभी कंपोज़ेबल फ़ंक्शन को स्किप कर सकता है. ज़्यादा जानकारी के लिए, अगर इनपुट में बदलाव नहीं हुआ है, तो स्किप करना लेख पढ़ें.

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

var padding by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    // The `padding` state is read in the composition phase
    // when the modifier is constructed.
    // Changes in `padding` will invoke recomposition.
    modifier = Modifier.padding(padding)
)

दूसरा चरण: लेआउट

लेआउट फ़ेज़ में दो चरण होते हैं: मेज़रमेंट और प्लेसमेंट. मेज़रमेंट चरण, Layout कॉम्पोज़ेबल, LayoutModifier इंटरफ़ेस के MeasureScope.measure तरीके वगैरह में पास किए गए मेज़रमेंट लैम्ब्डा को चलाता है. प्लेसमेंट चरण, layout फ़ंक्शन का प्लेसमेंट ब्लॉक, Modifier.offset { … } का लैम्ब्डा ब्लॉक वगैरह चलाता है.

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

ज़्यादा सटीक तरीके से बताएं, तो मेज़रमेंट चरण और प्लेसमेंट चरण के अलग-अलग फिर से शुरू करने के दायरे होते हैं. इसका मतलब है कि प्लेसमेंट चरण में स्टेटस पढ़ने से, उससे पहले मेज़रमेंट चरण फिर से शुरू नहीं होता. हालांकि, ये दोनों चरण अक्सर एक-दूसरे से जुड़े होते हैं. इसलिए, प्लेसमेंट चरण में पढ़ी गई स्थिति, मेज़रमेंट चरण से जुड़े अन्य रीस्टार्ट स्कोप पर असर डाल सकती है.

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // The `offsetX` state is read in the placement step
        // of the layout phase when the offset is calculated.
        // Changes in `offsetX` restart the layout.
        IntOffset(offsetX.roundToPx(), 0)
    }
)

तीसरा चरण: ड्रॉइंग

ड्रॉइंग कोड के दौरान स्टेटस पढ़ने से, ड्रॉइंग फ़ेज़ पर असर पड़ता है. आम तौर पर, Canvas(), Modifier.drawBehind, और Modifier.drawWithContent जैसे नाम इस्तेमाल किए जाते हैं. जब स्थिति की वैल्यू बदलती है, तो Compose यूज़र इंटरफ़ेस (यूआई) सिर्फ़ ड्रॉ फ़ेज़ चलाता है.

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // The `color` state is read in the drawing phase
    // when the canvas is rendered.
    // Changes in `color` restart the drawing.
    drawRect(color)
}

स्टेटस रीड को ऑप्टिमाइज़ करना

Compose, स्थानीय स्थिति की रीड ट्रैकिंग करता है. इसलिए, हम हर स्थिति को सही फ़ेज़ में पढ़कर, किए जाने वाले काम को कम कर सकते हैं.

आइए, एक उदाहरण देखें. यहां एक Image() है, जो अपने फ़ाइनल लेआउट की पोज़िशन को ऑफ़सेट करने के लिए, ऑफ़सेट मॉडिफ़ायर का इस्तेमाल करता है. इसकी वजह से, उपयोगकर्ता के स्क्रोल करने पर पैरलॅक्स इफ़ेक्ट दिखता है.

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        // Non-optimal implementation!
        Modifier.offset(
            with(LocalDensity.current) {
                // State read of firstVisibleItemScrollOffset in composition
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

    LazyColumn(state = listState) {
        // ...
    }
}

यह कोड काम करता है, लेकिन इससे परफ़ॉर्मेंस अच्छी नहीं होती. जैसा कि लिखा गया है, कोड firstVisibleItemScrollOffset स्टेटस की वैल्यू पढ़ता है और उसे Modifier.offset(offset: Dp) फ़ंक्शन को पास करता है. जब उपयोगकर्ता स्क्रोल करता है, तो firstVisibleItemScrollOffset वैल्यू बदल जाएगी. जैसा कि हम जानते हैं कि Compose, किसी भी स्टेटस को पढ़ने की जानकारी को ट्रैक करता है, ताकि वह पढ़ने के कोड को फिर से शुरू (फिर से शुरू) कर सके. हमारे उदाहरण में, यह कोड Box का कॉन्टेंट है.

यह कंपोज़िशन फ़ेज़ के दौरान, स्टेटस को पढ़ने का उदाहरण है. यह ज़रूरी नहीं है कि यह एक बुरी बात हो. असल में, यह फिर से कॉम्पोज़ करने का आधार है. इससे डेटा में हुए बदलावों की वजह से, नया यूज़र इंटरफ़ेस (यूआई) जनरेट होता है.

हालांकि, इस उदाहरण में यह सबसे सही नहीं है, क्योंकि हर स्क्रोल इवेंट के नतीजे में, पूरे कॉम्पोज़ेबल कॉन्टेंट का फिर से आकलन किया जाएगा. इसके बाद, उसे मेज़र किया जाएगा, लेआउट किया जाएगा, और आखिर में उसे ड्रॉ किया जाएगा. हम हर स्क्रोल पर कॉम्पोज़ फ़ेज़ को ट्रिगर कर रहे हैं. भले ही, क्या दिखाया जा रहा है उसमें कोई बदलाव नहीं हुआ है, सिर्फ़ कहां दिखाया जा रहा है उसमें बदलाव हुआ है. हम स्टेटस रीड को ऑप्टिमाइज़ करके, सिर्फ़ लेआउट फ़ेज़ को फिर से ट्रिगर कर सकते हैं.

ऑफ़सेट मॉडिफ़ायर का एक और वर्शन उपलब्ध है: Modifier.offset(offset: Density.() -> IntOffset).

यह वर्शन, lambda पैरामीटर लेता है. इसमें, lambda ब्लॉक से नतीजा के तौर पर ऑफ़सेट मिलता है. इसका इस्तेमाल करने के लिए, अपने कोड को अपडेट करें:

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // State read of firstVisibleItemScrollOffset in Layout
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState) {
        // ...
    }
}

तो यह ज़्यादा बेहतर परफ़ॉर्म क्यों करती है? हम मॉडिफ़ायर को जो लैम्ब्डा ब्लॉक देते हैं उसे लेआउट फ़ेज़ के दौरान (खास तौर पर, लेआउट फ़ेज़ के प्लेसमेंट चरण के दौरान) शुरू किया जाता है. इसका मतलब है कि कॉम्पोज़िशन के दौरान, firstVisibleItemScrollOffset स्टेटस नहीं पढ़ा जाता. Compose, स्टेटस पढ़ने के समय को ट्रैक करता है. इसलिए, इस बदलाव का मतलब है कि अगर firstVisibleItemScrollOffset की वैल्यू बदलती है, तो Compose को सिर्फ़ लेआउट और ड्रॉइंग के चरणों को फिर से शुरू करना होगा.

इस उदाहरण में, नतीजे के तौर पर मिलने वाले कोड को ऑप्टिमाइज़ करने के लिए, अलग-अलग ऑफ़सेट मॉडिफ़ायर का इस्तेमाल किया गया है. हालांकि, इसका सामान्य आइडिया सही है: स्टेटस रीड को कम से कम फ़ेज़ में लोकलाइज़ करने की कोशिश करें, ताकि Compose कम से कम काम कर सके.

हालांकि, कॉम्पोज़िशन के फ़ेज़ में स्टेटस पढ़ना ज़रूरी होता है. इसके बावजूद, कुछ मामलों में हम स्टेटस में हुए बदलावों को फ़िल्टर करके, फिर से कॉम्पोज़ करने की संख्या को कम कर सकते हैं. इस बारे में ज़्यादा जानकारी के लिए, derivedStateOf: एक या एक से ज़्यादा स्टेटस ऑब्जेक्ट को किसी दूसरे स्टेटस में बदलना देखें.

फिर से कॉम्पोज़ करने वाला लूप (साइकल के फ़ेज़ पर निर्भरता)

हमने पहले बताया था कि Compose के फ़ेज़ हमेशा एक ही क्रम में शुरू होते हैं. साथ ही, एक ही फ़्रेम में पीछे जाने का कोई तरीका नहीं है. हालांकि, इससे ऐप्लिकेशन को अलग-अलग फ़्रेम में कॉम्पोज़िशन लूप में जाने से नहीं रोका जा सकता. इस उदाहरण से समझें:

Box {
    var imageHeightPx by remember { mutableStateOf(0) }

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                // Don't do this
                imageHeightPx = size.height
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = with(LocalDensity.current) { imageHeightPx.toDp() }
        )
    )
}

यहां हमने वर्टिकल कॉलम को गलत तरीके से लागू किया है. इसमें सबसे ऊपर इमेज है और उसके नीचे टेक्स्ट है. हम इमेज का रिज़ॉल्यूशन जानने के लिए Modifier.onSizeChanged() का इस्तेमाल कर रहे हैं. इसके बाद, टेक्स्ट को नीचे ले जाने के लिए Modifier.padding() का इस्तेमाल कर रहे हैं. Px से Dp पर वापस जाने का अस्वाभाविक कन्वर्ज़न, पहले से ही यह दिखाता है कि कोड में कोई समस्या है.

इस उदाहरण में समस्या यह है कि हम एक ही फ़्रेम में "फ़ाइनल" लेआउट पर नहीं पहुंचते. यह कोड, एक से ज़्यादा फ़्रेम पर काम करता है. इससे यूज़र इंटरफ़ेस (यूआई) पर स्क्रीन पर यूज़र के लिए यूआई जंप होता है.

आइए, हर फ़्रेम को देखते हुए यह समझते हैं कि क्या हो रहा है:

पहले फ़्रेम के कॉम्पोज़िशन फ़ेज़ में, imageHeightPx की वैल्यू 0 होती है और टेक्स्ट को Modifier.padding(top = 0) के साथ दिया जाता है. इसके बाद, लेआउट वाला चरण शुरू होता है और onSizeChanged मॉडिफ़ायर के लिए कॉलबैक को कॉल किया जाता है. ऐसा तब होता है, जब imageHeightPx को इमेज की असल ऊंचाई पर अपडेट किया जाता है. अगले फ़्रेम के लिए, शेड्यूल किए गए रीकंपोज़िशन को कंपोज करें. ड्रॉइंग के फ़ेज़ में, टेक्स्ट को 0 पैडिंग के साथ रेंडर किया जाता है, क्योंकि वैल्यू में बदलाव अभी तक नहीं हुआ है.

इसके बाद, imageHeightPx की वैल्यू में हुए बदलाव के हिसाब से शेड्यूल किया गया दूसरा फ़्रेम शुरू हो जाता है. स्टेटस को Box कॉन्टेंट ब्लॉक में पढ़ा जाता है और इसे कॉम्पोज़िशन फ़ेज़ में शुरू किया जाता है. इस बार, टेक्स्ट को इमेज की ऊंचाई के हिसाब से पैडिंग दी गई है. लेआउट फ़ेज़ में, कोड imageHeightPx की वैल्यू फिर से सेट करता है. हालांकि, वैल्यू एक जैसी रहने की वजह से, फिर से कॉम्पोज़ करने का कोई शेड्यूल नहीं किया जाता.

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

यह उदाहरण बनावटी लग सकता है, लेकिन इस सामान्य पैटर्न से सावधान रहें:

  • Modifier.onSizeChanged(), onGloballyPositioned() या लेआउट के कुछ अन्य ऑपरेशन
  • किसी स्टेटस को अपडेट करना
  • उस स्थिति का इस्तेमाल,लेआउट मॉडिफ़ायर (padding(), height() या मिलते-जुलते) के इनपुट के तौर पर करें
  • दोहराए जाने की संभावना

ऊपर दिए गए सैंपल को ठीक करने के लिए, सही लेआउट प्राइमिटिव का इस्तेमाल करें. ऊपर दिए गए उदाहरण को, Column() के साथ लागू किया जा सकता है. हालांकि, आपके पास ज़्यादा जटिल उदाहरण हो सकता है, जिसके लिए कस्टम लेआउट लिखना ज़रूरी होगा. ज़्यादा जानकारी के लिए, कस्टम लेआउट की गाइड देखें.

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