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

आपको कंपोज़ करने से जुड़ी सामान्य समस्याएं आ सकती हैं. इन गलतियों की वजह से, आपको ऐसा कोड मिल सकता है जो ठीक से काम करता हो. हालांकि, इससे आपके यूज़र इंटरफ़ेस (यूआई) की परफ़ॉर्मेंस पर बुरा असर पड़ सकता है. 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 को पहली बार कंपोज़ करते समय, सूची को एक बार क्रम से लगाया जाता है. अगर संपर्क या तुलना करने वाले व्यक्ति में बदलाव होता है, तो क्रम से लगाई गई सूची फिर से जनरेट होती है. ऐसा न होने पर, कंपोज़ेबल, कैश मेमोरी में सेव की गई क्रम से लगाई गई सूची का इस्तेमाल जारी रख सकता है.

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

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

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

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

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

आपकी मदद के बिना, कंपोज़ करने की सुविधा को यह पता नहीं चलता कि सूची में बिना बदलाव किए गए आइटम सिर्फ़ हटाए जा रहे हैं. इसके बजाय, 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)
        }
    }
}

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

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

val listState = rememberLazyListState()

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

val showButton = listState.firstVisibleItemIndex > 0

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

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

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

val listState = rememberLazyListState()

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

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

AnimatedVisibility(visible = showButton) {
    ScrollToTopButton()
}

पढ़ने की प्रोसेस को ज़्यादा से ज़्यादा समय के लिए रोकना

परफ़ॉर्मेंस की समस्या का पता चलने पर, स्टेट को पढ़ने की प्रोसेस को कुछ समय के लिए रोका जा सकता है. स्टेट को पढ़ने की प्रोसेस को कुछ समय के लिए रोकने से, यह पक्का किया जा सकेगा कि कंपोज़िशन के दौरान, 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 composable है. ध्यान दें कि 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)
    ) {
        // ...
    }
}

स्क्रोल पैरामीटर अब लैम्डा है. इसका मतलब है कि Title अब भी होइस्ट की गई स्थिति को रेफ़रंस कर सकता है. हालांकि, वैल्यू को सिर्फ़ Title में पढ़ा जाता है, जहां इसकी ज़रूरत होती है. इस वजह से, स्क्रोल वैल्यू बदलने पर, सबसे नज़दीकी रइकंपोज़िशन स्कोप अब Title कंपोज़ेबल है. कंपोज़ को अब पूरे 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>
}

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

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

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

अन्य संसाधन