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

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

लेज़ी लेआउट की कुंजियों का इस्तेमाल करना

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

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

@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)
        }
    }
}

रीकंपोज़िशन को सीमित करने के लिए, 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 कंपोज़ेबल है. ध्यान दें कि 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 कंपोज़ेबल है. 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 के पिछले उदाहरण में.

अन्य संसाधन