सबसे अच्छे तरीके आज़माएं

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

ज़्यादा कंप्यूटेशनल पावर वाले कैलकुलेशन को कम करने के लिए, remember का इस्तेमाल करें

कंपोज़ेबल फ़ंक्शन बार-बार चल सकते हैं. ये किसी ऐनिमेशन के हर फ़्रेम के लिए भी चल सकते हैं. इस वजह से, आपको कंपोज़ेबल के मुख्य हिस्से में कम से कम कैलकुलेशन करना चाहिए.

एक अहम तकनीक है, remember की मदद से कैलकुलेशन के नतीजों को स्टोर करना. इस तरह, कैलकुलेशन एक बार चलती है और जब भी ज़रूरत हो, तब नतीजे फ़ेच किए जा सकते हैं.

उदाहरण के लिए, यहां कुछ ऐसा कोड दिया गया है जो नामों की क्रम से लगाई गई सूची दिखाता है. हालांकि, यह क्रम से लगाने का काम बहुत ही ज़्यादा रिसॉर्स खर्च करके करता है:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    LazyColumn(modifier) {
        // DON’T DO THIS
        items(contacts.sortedWith(comparator)) { contact ->
            // ...
        }
    }
}

जब भी ContactsList को फिर से कंपोज़ किया जाता है, तो पूरी संपर्क सूची को फिर से क्रम में लगाया जाता है. भले ही, सूची में कोई बदलाव न हुआ हो. अगर उपयोगकर्ता सूची को स्क्रोल करता है, तो कोई नई लाइन दिखने पर कंपोज़ेबल को फिर से कंपोज़ किया जाता है.

इस समस्या को हल करने के लिए, सूची को LazyColumn के बाहर क्रम से लगाएं. इसके बाद, क्रम से लगाई गई सूची को remember के साथ सेव करें:

@Composable
fun ContactList(
    contacts: List<Contact>,
    comparator: Comparator<Contact>,
    modifier: Modifier = Modifier
) {
    val sortedContacts = remember(contacts, comparator) {
        contacts.sortedWith(comparator)
    }

    LazyColumn(modifier) {
        items(sortedContacts) {
            // ...
        }
    }
}

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

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

किसी बड़े कलेक्शन या महंगे ऑब्जेक्ट को पास करते समय सावधानी बरतें equals() कम खर्च वाले कैलकुलेशन के लिए remember की कुंजी के तौर पर लागू करना, क्योंकि कुंजी की तुलना करने पर, सीधे कैलकुलेशन करने से ज़्यादा खर्च आ सकता है:

@Composable
fun ContactHeader(contacts: List<Contact>) {
    // Avoid: Comparing the `contacts` list with .equals() on recomposition
    // is more expensive than reading contacts.size or contacts.firstOrNull() directly.
    val firstContactName = remember(contacts) {
        contacts.firstOrNull()?.name.orEmpty()
    }
    Text(firstContactName)
}

लेज़ी लेआउट की और कॉन्टेंट टाइप का इस्तेमाल करना

लेज़ी लेआउट, आइटम का फिर से इस्तेमाल करते हैं. ये आइटम को सिर्फ़ तब फिर से जनरेट या रीकंपोज़ करते हैं, जब ज़रूरत होती है. हालांकि, लेज़ी लेआउट को ऑप्टिमाइज़ किया जा सकता है, ताकि रीकंपोज़िशन और स्लॉट का फिर से इस्तेमाल किया जा सके.

मान लें कि उपयोगकर्ता की किसी कार्रवाई की वजह से, सूची में मौजूद कोई आइटम अपनी जगह से हट जाता है. उदाहरण के लिए, मान लें कि आपने बदलाव के समय के हिसाब से क्रम में लगाए गए नोट की सूची दिखाई है. इसमें, सबसे हाल ही में बदलाव किया गया नोट सबसे ऊपर है.

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes
        ) { note ->
            NoteRow(note)
        }
    }
}

हालांकि, इस कोड में कोई समस्या है. मान लें कि सबसे नीचे मौजूद नोट में बदलाव किया गया है. अब यह सबसे हाल ही में बदला गया नोट है. इसलिए, यह सूची में सबसे ऊपर चला जाता है और बाकी सभी नोट एक-एक स्थान नीचे चले जाते हैं.

आपकी मदद के बिना, Compose को यह पता नहीं चलता कि सूची में मौजूद आइटम में कोई बदलाव नहीं किया गया है, बल्कि उन्हें सिर्फ़ मूव किया गया है. इसके बजाय, Compose को लगता है कि पुराना "आइटम 2" मिटा दिया गया था और आइटम 3, आइटम 4, और बाकी सभी आइटम के लिए एक नया आइटम बनाया गया था. इसका नतीजा यह होता है कि Compose, सूची में मौजूद हर आइटम को फिर से कंपोज़ करता है. भले ही, उनमें से सिर्फ़ एक आइटम में बदलाव हुआ हो.

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

@Composable
fun NotesList(notes: List<Note>) {
    LazyColumn {
        items(
            items = notes,
            key = { note ->
                // Return a stable, unique key for the note
                note.id
            }
        ) { note ->
            NoteRow(note)
        }
    }
}

लेज़ी लेआउट (LazyColumn, LazyRow, LazyGrid) के साथ काम करते समय, इन दिशा-निर्देशों का भी ध्यान रखें:

  • बंडल में सेव की जा सकने वाली यूनीक कुंजियों का इस्तेमाल करें: कुंजियां यूनीक, डिटरमिनिस्टिक, और Android Bundle में सेव की जा सकने वाली होनी चाहिए. ऐसा इसलिए, क्योंकि लेज़ी लेआउट, अंदरूनी तौर पर rememberSaveable का इस्तेमाल करते हैं. Int, Long या String जैसे प्रिमिटिव टाइप को प्राथमिकता दें. साथ ही, सूची के इंडेक्स या hashCode() को कभी भी कुंजियों के तौर पर इस्तेमाल न करें.
  • अलग-अलग तरह के आइटम वाली सूचियों के लिए, contentType हमेशा तय करें: contentType = { it.type } को items या itemsIndexed में पास करें, ताकि Compose, स्क्रोल करने के दौरान एक जैसे विज़ुअल स्ट्रक्चर वाले आइटम के बीच ही कंपोज़िशन स्लॉट का फिर से इस्तेमाल करे.
  • indexOf के बजाय itemsIndexed का इस्तेमाल करें: items ब्लॉक के अंदर list.indexOf(item) को कॉल करने से बचें. इससे लेआउट, O(N²) स्कैन में बदल जाता है. इसके बजाय, itemsIndexed का इस्तेमाल करें.
  • शेयर की गई स्थिति और CompositionLocal रीड को होस्ट करना: लेज़ी लेआउट के ऊपर मौजूद पैरंट कंपोज़ेबल में, शेयर किए गए Flow इंस्टेंस इकट्ठा करें और CompositionLocal वैल्यू (जैसे, LocalContext.current या LocalDensity.current) को एक बार पढ़ें. इसके बाद, रिज़ॉल्व की गई वैल्यू को आइटम कंपोज़ेबल में पास करें.
  • छोटी और सीमित सूचियों के लिए, Row या Column का इस्तेमाल करें: छोटे स्टैटिक या सीमित कलेक्शन (जैसे, 1 से 10 आइटम) के लिए, forEach के साथ स्टैंडर्ड Row या Column का इस्तेमाल करने से, लेज़ी वर्चुअलाइज़ेशन और प्रीफ़ेचिंग ओवरहेड से बचा जा सकता है.

रीकंपोज़िशन को सीमित करने के लिए, computedStateOf या derivedStateOf का इस्तेमाल करें

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

val listState = rememberLazyListState()

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

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

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

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

val listState = rememberLazyListState()

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

val showButton by remember {
    derivedStateOf {
        listState.firstVisibleItemIndex > 0
    }
}

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

Compose के अपने वर्शन और कैलकुलेशन की लागत के आधार पर, स्टेट डेरीवेशन का सही तरीका चुनें:

  • remember { computedStateOf { ... } } (Compose Runtime 1.13 और इसके बाद के वर्शन): यह हर बार पढ़े जाने पर, नतीजे को कैश मेमोरी में सेव किए बिना, अपनी कैलकुलेशन का फिर से आकलन करता है. साथ ही, यह कंपोज़िशन को सिर्फ़ तब अमान्य करता है, जब कैलकुलेट किया गया आउटपुट बदलता है. इससे कैश मेमोरी की बुककीपिंग और अमान्य राइट-बैक ओवरहेड से बचा जा सकता है. इसलिए, बुनियादी शर्तों की जांच और तेज़ी से बदलने वाली स्थितियों (जैसे, स्क्रोल ऑफ़सेट) से मिलने वाले लाइटवेट डेरिवेशन के लिए, remember { computedStateOf { listState.firstVisibleItemIndex > 0 } } को प्राथमिकता दें.
  • remember { derivedStateOf { ... } }: यह कई बार पढ़ने के दौरान, कैलकुलेशन के नतीजे को कैश मेमोरी में सेव करता है. derivedStateOf का इस्तेमाल, Compose के 1.13 से पहले के वर्शन पर करें. इसके अलावा, इसका इस्तेमाल तब करें, जब कैलकुलेशन में ज़्यादा कंप्यूटेशनल पावर की ज़रूरत हो और कंपोज़िशन पास के दौरान इसे कई बार पढ़ा जाए. derivedStateOf में रीड की गई स्नैपशॉट स्टेट वैल्यू (जैसे, listState.firstVisibleItemIndex) को remember की के तौर पर पास न करें. ऐसा करने से, हर बार स्टेट बदलने पर DerivedState फिर से बन जाता है और स्किप नहीं किया जा सकता.
  • Kotlin के सामान्य गेटर (get()) और प्रॉपर्टी को सीधे तौर पर पढ़ना: सामान्य बूलियन एक्सप्रेशन के लिए, कस्टम गेटर का इस्तेमाल करें. ऐसा तब करें, जब अमान्य होने की स्थिति को कम करने की ज़रूरत न हो. साथ ही, कलेक्शन .size को derivedStateOf में रैप करने के बजाय, सीधे तौर पर पढ़ें. सिंक्रोनस स्टेट डेरिवेशन के लिए, snapshotFlow के साथ produceState का इस्तेमाल करने से बचें. ऐसा इसलिए, क्योंकि इससे ग़ैर-ज़रूरी कोरूटीन डिस्पैच ओवरहेड जुड़ जाता है.

रीड ऑपरेशन को ज़्यादा से ज़्यादा समय तक रोकें

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

इस इफ़ेक्ट को पाने के लिए, Title कंपोज़ेबल को स्क्रोल ऑफ़सेट की ज़रूरत होती है, ताकि Modifier का इस्तेमाल करके खुद को ऑफ़सेट किया जा सके. यहां Jetsnack के कोड का आसान वर्शन दिया गया है. इसे ऑप्टिमाइज़ करने से पहले का वर्शन:

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack, scroll.value)
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
    // ...
    val offset = with(LocalDensity.current) { scroll.toDp() }

    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

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

@Composable
fun SnackDetail() {
    // ...

    Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
        val scroll = rememberScrollState(0)
        // ...
        Title(snack) { scroll.value }
        // ...
    } // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset)
    ) {
        // ...
    }
}

स्क्रोल पैरामीटर अब एक lambda है. इसका मतलब है कि Title अब भी होस्ट की गई स्थिति को रेफ़र कर सकता है. हालांकि, वैल्यू को सिर्फ़ Title में पढ़ा जाता है, जहां इसकी असल में ज़रूरत होती है. इस वजह से, स्क्रोल वैल्यू बदलने पर, सबसे नज़दीकी रीकंपोज़िशन स्कोप अब Title कंपोज़ेबल है. Compose को अब पूरे Box को रीकंपोज़ करने की ज़रूरत नहीं है.

यह एक अच्छा सुधार है, लेकिन आप इससे बेहतर कर सकते हैं! अगर आपको किसी कंपोज़ेबल को फिर से लेआउट करने या फिर से ड्रॉ करने के लिए, फिर से कंपोज़ करना पड़ रहा है, तो आपको सावधान हो जाना चाहिए. इस मामले में, सिर्फ़ Title कंपोज़ेबल का ऑफ़सेट बदला जा रहा है. इसे लेआउट फ़ेज़ में किया जा सकता है.

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    Column(
        modifier = Modifier
            .offset { IntOffset(x = 0, y = scrollProvider()) }
    ) {
        // ...
    }
}

पहले, कोड में Modifier.offset(x: Dp, y: Dp) का इस्तेमाल किया जाता था. यह ऑफ़सेट को पैरामीटर के तौर पर लेता है. मॉडिफ़ायर के लैम्ब्डा वर्शन पर स्विच करके, यह पक्का किया जा सकता है कि फ़ंक्शन, लेआउट फ़ेज़ में स्क्रोल की स्थिति को रीड करे. इस वजह से, स्क्रोल की स्थिति बदलने पर, Compose कंपोज़िशन के फ़ेज़ को पूरी तरह से स्किप कर सकता है. इसके बाद, वह सीधे लेआउट के फ़ेज़ पर जा सकता है. जब बार-बार बदलने वाले स्टेट वैरिएबल को मॉडिफ़ायर में पास किया जाता है, तो आपको जब भी संभव हो, मॉडिफ़ायर के लैम्डा वर्शन का इस्तेमाल करना चाहिए.

इस तरीके का एक और उदाहरण यहां दिया गया है. इस कोड को अब तक ऑप्टिमाइज़ नहीं किया गया है:

// Here, assume animateColorBetween() is a function that swaps between
// two colors
val color by animateColorBetween(Color.Cyan, Color.Magenta)

Box(
    Modifier
        .fillMaxSize()
        .background(color)
)

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

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

val color by animateColorBetween(Color.Cyan, Color.Magenta)
Box(
    Modifier
        .fillMaxSize()
        .drawBehind {
            drawRect(color)
        }
)

उल्टे क्रम में लिखने से बचें

Compose का मुख्य सिद्धांत यह है कि आपको कभी भी ऐसी स्थिति में नहीं लिखना चाहिए जिसे पहले ही पढ़ा जा चुका है. ऐसा करने पर, इसे बैकवर्ड राइट कहा जाता है. इससे हर फ़्रेम पर, बार-बार रीकंपोज़िशन हो सकता है.

नीचे दिए गए कंपोज़ेबल में, इस तरह की गड़बड़ी का उदाहरण दिखाया गया है.

@Composable
fun BadComposable() {
    var count by remember { mutableIntStateOf(0) }

    // Causes recomposition on click
    Button(onClick = { count++ }, Modifier.wrapContentSize()) {
        Text("Recompose")
    }

    Text("$count")
    count++ // Backwards write, writing to state after it has been read</b>
}

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

कंपोज़िशन में स्टेट को कभी भी न लिखकर, बैकवर्ड राइट से पूरी तरह बचा जा सकता है. अगर मुमकिन हो, तो किसी इवेंट के जवाब में हमेशा स्टेट में लिखें. साथ ही, पिछले onClick उदाहरण की तरह लैम्ब्डा में लिखें.

बैकवर्ड राइट, रीकंपोज़िशन लूप, और फ़ेज़ कोऑर्डिनेशन के बारे में ज़्यादा जानने के लिए, Compose में बैकवर्ड राइट लेख पढ़ें.

अन्य संसाधन