जब आपको किसी ऐसी अस्थिर क्लास का सामना करना पड़े जिससे परफ़ॉर्मेंस की समस्याएं होती हैं, तो आपको यह ऑप्टिमाइज़ करना चाहिए कि Compose, उसकी तुलना कैसे करता है या उसे कैसे इंस्टैंशिएट करता है. इस दस्तावेज़ में, ऐसा करने के लिए इस्तेमाल की जा सकने वाली कई तकनीकों के बारे में बताया गया है.
स्ट्रॉन्ग स्किपिंग की सुविधा चालू करना
Kotlin 2.0.20 से, स्ट्रॉन्ग स्किपिंग मोड डिफ़ॉल्ट रूप से चालू रहता है.
स्ट्रॉन्ग स्किपिंग मोड की मदद से, अस्थिर पैरामीटर वाले कंपोज़ेबल को स्किप किया जा सकता है. इसके लिए, इंस्टेंस इक्वैलिटी (===) का इस्तेमाल करके अस्थिर पैरामीटर की तुलना की जाती है. वहीं, स्ट्रक्चरल इक्वैलिटी (Object.equals()) का इस्तेमाल करके स्थिर पैरामीटर की तुलना की जाती है.
अगर Kotlin के 2.0.20 से पहले के वर्शन का इस्तेमाल किया जा रहा है, तो पहले स्ट्रॉन्ग स्किपिंग मोड चालू करें. अगर स्ट्रॉन्ग स्किपिंग मोड पहले से चालू है और कोई कंपोज़ेबल अब भी बेवजह फिर से कंपोज़ हो रहा है, तो देखें कि क्या हर बार फिर से कंपोज़ होने पर, अनस्टेबल पैरामीटर के नए इंस्टेंस असाइन किए जा रहे हैं.
ज़्यादा जानकारी के लिए, स्ट्रॉन्ग स्किपिंग देखें.
क्लास को इम्यूटेबल बनाना
आपके पास किसी अनस्टेबल क्लास को पूरी तरह से इम्यूटेबल बनाने का विकल्प भी होता है, ताकि कंपाइलर उसकी स्टेबिलिटी का पता अपने-आप लगा सके.
- इम्यूटेबल: इससे ऐसे टाइप का पता चलता है जिसमें किसी भी प्रॉपर्टी की वैल्यू, उस टाइप का इंस्टेंस बनने के बाद कभी नहीं बदल सकती. साथ ही, सभी तरीके रेफ़रंस के तौर पर पारदर्शी होते हैं.
- पक्का करें कि क्लास की सभी प्रॉपर्टी
varके बजायvalहों. साथ ही, वे इम्यूटेबल टाइप की हों. String,Int, औरFloatजैसे प्रिमिटिव टाइप को कभी बदला नहीं जा सकता.- Compose कंपाइलर की मदद से बनाए गए मॉड्यूल में,
valप्रिमिटिव याStringप्रॉपर्टी वाले फ़्लैटdata classटाइप को पहले से ही स्टेबल माना जाता है. साथ ही, उनकी तुलना.equals()से अपने-आप की जाती है—आपको उन्हें@Immutableके साथ एनोटेट करने की ज़रूरत नहीं है. - अगर इम्यूटेबिलिटी मुमकिन नहीं है, तो आपको किसी भी म्यूटेबल प्रॉपर्टी के लिए, Compose स्टेट का इस्तेमाल करना होगा.
- पक्का करें कि क्लास की सभी प्रॉपर्टी
- स्टेबल: इससे ऐसे टाइप का पता चलता है जिसमें बदलाव किया जा सकता है. Compose रनटाइम को इस बात की जानकारी नहीं होती कि टाइप की किसी भी सार्वजनिक प्रॉपर्टी या तरीके के व्यवहार से, पिछली बार के मुकाबले अलग नतीजे कब और कैसे मिलेंगे.
कलेक्शन और स्थिरता
Compose किसी क्लास को अनस्टेबल क्यों मानता है, इसकी एक सामान्य वजह कलेक्शन है. स्टेबिलिटी से जुड़ी समस्याओं का विश्लेषण करना पेज पर बताया गया है कि Compose कंपाइलर को इस बात का पूरा भरोसा नहीं हो सकता कि List, Map, और Set जैसे कलेक्शन इंटरफ़ेस, पूरी तरह से इम्यूटेबल हैं. इसलिए, वह उन्हें अनस्टेबल के तौर पर मार्क करता है.
ऐप्लिकेशन के क्रैश होने की समस्याओं का पता लगाने की गाइड में दी गई इस क्लास को देखें:
unstable class Snack {
…
unstable val tags: Set<String>
…
}
स्ट्रॉन्ग स्किपिंग मोड चालू होने पर (Kotlin 2.0.20 और इसके बाद के वर्शन में डिफ़ॉल्ट रूप से चालू होता है), अनस्टेबल कलेक्शन और कलेक्शन वाली क्लास, रीकंपोज़िशन को पहले ही स्किप कर देती हैं. इसके लिए, वे इंस्टेंस इक्वालिटी (===) का इस्तेमाल करती हैं. ऐसा तब होता है, जब एक ही रेफ़रंस पास किया जाता है.
kotlin.collections.List, Set या Map को Kotlinx Immutable Collections (ImmutableList, ImmutableSet, ImmutableMap) से बदलने से पहले सावधानी बरतें. इसके अलावा, कलेक्शन होल्डिंग मॉडल में @Immutable, @Stable या stability_config.conf नियम जोड़ते समय भी सावधानी बरतें. स्ट्रॉन्ग स्किपिंग मोड में, स्टैंडर्ड List, रेफ़रंस इक्वालिटी (===) का इस्तेमाल करके स्किप करता है. वहीं, ImmutableList और एनोटेट किए गए कलेक्शन मॉडल, हर बार फिर से कॉम्पोज़ करने पर, एलिमेंट-दर-एलिमेंट .equals() तुलना करते हैं.
बड़े कलेक्शन या लेज़ी लेआउट (LazyColumn, LazyRow, LazyGrid) के लिए, हर आइटम पर .equals() की जांच करने में, फिर से कंपोज़ करने की तुलना में ज़्यादा समय लग सकता है. अगर ImmutableList प्रॉपर्टी को data class से चाइल्ड कंपोज़ेबल में पास किया जाता है, तो यह लागत दोगुनी हो जाती है:
data class SnackCategory(
val name: String,
val snacks: ImmutableList<Snack>
)
@Composable
fun CategorySection(category: SnackCategory) {
Text(category.name)
// Compose compares category.snacks twice: once in SnackCategory.equals()
// for CategorySection, and again for HighlightedSnacks.
HighlightedSnacks(snacks = category.snacks)
}
कलेक्शन पर स्ट्रक्चरल समानता लागू करने के बजाय, एमिशन के दौरान कलेक्शन के इंस्टेंस रेफ़रंस को सुरक्षित रखें. उदाहरण के लिए, अपने ViewModel में या remember के साथ ट्रांसफ़ॉर्मेशन को कैश मेमोरी में सेव करके. इससे Compose, इंस्टेंस की समानता (===) का इस्तेमाल करने से बच सकता है. कलेक्शन को होल्ड करने वाले मॉडल पर ImmutableList या स्टेबिलिटी एनोटेशन का इस्तेमाल सिर्फ़ तब करें, जब कलेक्शन के इंस्टेंस रेफ़रंस को सुरक्षित न रखा जा सके और मानदंड यह पुष्टि करें कि स्ट्रक्चरल समानता की जांच, फिर से कंपोज़ करने की तुलना में तेज़ है.
अगर Kotlin का 2.0.20 से पुराना वर्शन इस्तेमाल किया जा रहा है, तो पहले स्ट्रॉन्ग स्किपिंग मोड चालू करें.
Stable या Immutable की मदद से एनोटेट करना
छोटी क्लास के लिए, स्टेबिलिटी से जुड़ी समस्याओं को हल करने का एक तरीका यह है कि अनस्टेबल क्लास को @Stable या @Immutable से एनोटेट किया जाए. इसके लिए, .equals() की मदद से जांच की जा सकती है.
किसी क्लास को एनोटेट करने से, कंपाइलर आपकी क्लास के बारे में जो अनुमान लगाता है वह बदल जाता है. यह !! Kotlin में ऑपरेटर की तरह है. आपको इन एनोटेशन का इस्तेमाल करते समय बहुत सावधानी बरतनी चाहिए. कंपाइलर के व्यवहार को बदलने से, आपको ऐसी गड़बड़ियां मिल सकती हैं जिनके बारे में आपको पहले से पता नहीं था. जैसे, कंपोज़ेबल को फिर से कंपोज़ न करना.
स्ट्रॉन्ग स्किपिंग मोड चालू होने पर, @Stable या @Immutable जोड़ने का फ़ैसला लेते समय इन दिशा-निर्देशों का पालन करें:
- उन क्लास के बारे में जानकारी न दें जिन्हें पहले से ही स्टेबल माना गया है: Compose कंपाइलर की मदद से बनाए गए मॉड्यूल में, स्टेबल टाइप की
valप्रॉपर्टी वाली क्लास को पहले से ही स्टेबल माना जाता है. - इंस्टेंस फिर से बनाए जाने पर, सिर्फ़ उन क्लास के बारे में बताएं जिनकी
.equals()जांच करने में ज़्यादा समय और मेमोरी खर्च न हो: जब कंपाइलर, स्टेबिलिटी का अनुमान न लगा पाए, तो@Stableया@Immutableजोड़ें. उदाहरण के लिए, नॉन-Compose मॉड्यूल के इंटरफ़ेस या क्लास. साथ ही, डेटा सोर्स हर बार डेटा भेजने पर, उन आइटम के लिए नए ऑब्जेक्ट इंस्टेंस असाइन करता है जिनमें कोई बदलाव नहीं हुआ है. हालांकि, इसके लिए ज़रूरी है कि क्लास छोटी हो और उसकी.equals()जांच करने में ज़्यादा समय और मेमोरी खर्च न हो. - कलेक्शन वाले मॉडल का इस्तेमाल सावधानी से करें: बड़े कलेक्शन (
List,Set,Map) वाले डेटा मॉडल में@Stableया@Immutableजोड़ने से बचें. खास तौर पर, लेज़ी लेआउट (LazyColumn,LazyRow,LazyGrid,Pager) में इस्तेमाल किए जाने वाले मॉडल के लिए. इन मॉडल को एनोटेट न करने पर, Compose हर एलिमेंट में स्ट्रक्चरल.equals()जांच करने के बजाय, इंस्टेंस की समानता (===) का इस्तेमाल करके उनकी तुलना कर सकता है.
यहां दिए गए स्निपेट में, फ़्लैट डेटा क्लास (बिना कलेक्शन प्रॉपर्टी वाली) का एक छोटा उदाहरण दिया गया है. इसे किसी ऐसे मॉड्यूल में तय किए जाने पर, इम्यूटेबल के तौर पर एनोटेट किया जाता है जहां कंपाइलर, स्टेबिलिटी का अनुमान नहीं लगा सकता:
@Immutable
data class SnackSummary(
val id: Long,
val name: String,
val price: Long
)
चाहे @Immutable या @Stable एनोटेशन का इस्तेमाल किया जाए, Compose कंपाइलर SnackSummary क्लास को स्टेबल के तौर पर मार्क करता है और Object.equals() का इस्तेमाल करके इसकी तुलना करता है.
कलेक्शन में मौजूद एनोटेट की गई क्लास
एक ऐसे कंपोज़ेबल पर विचार करें जिसमें List<Snack> टाइप का पैरामीटर शामिल हो:
restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
stable index: Int
unstable snacks: List<Snack>
stable onSnackClick: Function1<Long, Unit>
stable modifier: Modifier? = @static Companion
)
भले ही, Snack स्टेबल हो या उसे @Immutable के साथ एनोटेट किया गया हो, Compose कंपाइलर अब भी HighlightedSnacks में मौजूद snacks पैरामीटर को unstable के तौर पर मार्क करता है. ऐसा इसलिए होता है, क्योंकि List एक इंटरफ़ेस है.
स्ट्रॉन्ग स्किपिंग मोड चालू होने पर भी, HighlightedSnacks को skippable के तौर पर मार्क किया जाता है. साथ ही, एक ही List इंस्टेंस पास किए जाने पर, इंस्टेंस की समानता (===) का इस्तेमाल करके, रीकंपोज़िशन को स्किप कर दिया जाता है. snacks पैरामीटर को stable बनाने के लिए, List<Snack> को @Immutable रैपर क्लास में रैप करें, List<Snack> को ImmutableList<Snack> से बदलें या kotlin.collections.* को अपनी स्टेबिलिटी कॉन्फ़िगरेशन फ़ाइल में जोड़ें.
हालांकि, ऐसा करने पर Compose, हर बार फिर से कॉम्पोज़ करने पर, एलिमेंट के हिसाब से .equals() की तुलना करता है. इन तरीकों का इस्तेमाल सिर्फ़ तब करें, जब सूची के इंस्टेंस रेफ़रंस को सुरक्षित न रखा जा सके और मानदंड से कोई फ़ायदा मिले.
स्टेबिलिटी कॉन्फ़िगरेशन फ़ाइल
Compose Compiler 1.5.5 से, कंपाइल के समय क्लास की कॉन्फ़िगरेशन फ़ाइल दी जा सकती है, ताकि उन्हें स्टेबल माना जा सके. इससे उन क्लास को स्टेबल माना जा सकता है जिन्हें कंट्रोल नहीं किया जा सकता. जैसे, LocalDateTime जैसी स्टैंडर्ड लाइब्रेरी क्लास.
कॉन्फ़िगरेशन फ़ाइल, एक सामान्य टेक्स्ट फ़ाइल होती है. इसमें हर लाइन में एक क्लास होती है. टिप्पणियां, सिंगल, और डबल वाइल्डकार्ड इस्तेमाल किए जा सकते हैं.
कॉन्फ़िगरेशन का उदाहरण:
// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>
इस सुविधा को चालू करने के लिए, कॉन्फ़िगरेशन फ़ाइल का पाथ, Compose कंपाइलर Gradle प्लग इन कॉन्फ़िगरेशन के composeCompiler विकल्प ब्लॉक में पास करें.
composeCompiler {
stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}
Compose कंपाइलर, आपके प्रोजेक्ट के हर मॉड्यूल पर अलग-अलग चलता है. इसलिए, ज़रूरत पड़ने पर अलग-अलग मॉड्यूल के लिए अलग-अलग कॉन्फ़िगरेशन उपलब्ध कराए जा सकते हैं. इसके अलावा, अपने प्रोजेक्ट के रूट लेवल पर एक कॉन्फ़िगरेशन रखें और हर मॉड्यूल को वह पाथ पास करें.
एक से ज़्यादा मॉड्यूल
एक और आम समस्या, मल्टी-मॉड्यूल आर्किटेक्चर से जुड़ी है. Compose कंपाइलर सिर्फ़ तब यह अनुमान लगा सकता है कि कोई क्लास स्टेबल है या नहीं, जब वह जिन नॉन-प्रिमिटिव टाइप को रेफ़र करती है उन्हें साफ़ तौर पर स्टेबल के तौर पर मार्क किया गया हो या वे ऐसे मॉड्यूल में हों जिसे Compose कंपाइलर की मदद से भी बनाया गया हो.
अगर आपकी डेटा लेयर, यूज़र इंटरफ़ेस (यूआई) लेयर से अलग मॉड्यूल में है, तो आपको यह समस्या आ सकती है. हालांकि, हमारा सुझाव है कि आप डेटा लेयर को यूज़र इंटरफ़ेस (यूआई) लेयर से अलग मॉड्यूल में ही रखें. स्ट्रॉन्ग स्किपिंग मोड में, Compose के अलावा अन्य मॉड्यूल की क्लास को अनस्टेबल माना जाता है. साथ ही, एक ही इंस्टेंस पास किए जाने पर, इंस्टेंस की समानता (===) का इस्तेमाल करके, फिर से कॉम्पोज़ करने की प्रोसेस को स्किप किया जाता है. हालांकि, अगर आपकी डेटा लेयर अक्सर छोटे, फ़्लैट मॉडल के नए इंस्टेंस को हर एमिशन पर एक जैसी वैल्यू के साथ असाइन करती है, तो स्ट्रक्चरल इक्वालिटी (Object.equals()) का विकल्प चुना जा सकता है.
समाधान
इस समस्या को हल करने के लिए, इनमें से कोई एक तरीका अपनाया जा सकता है:
- अपनी कंपाइलर कॉन्फ़िगरेशन फ़ाइल में, कम कीमत वाली
.equals()जांचों के साथ छोटी और फ़्लैट क्लास जोड़ें. - अपने डेटा लेयर मॉड्यूल पर Compose कंपाइलर को चालू करें या ज़रूरत के हिसाब से अपनी क्लास को
@Stableया@Immutableसे टैग करें.- इसके लिए, आपको अपनी डेटा लेयर में Compose डिपेंडेंसी जोड़नी होगी. हालांकि, यह सिर्फ़ Compose रनटाइम के लिए डिपेंडेंसी है, न कि
Compose-UIके लिए.
- इसके लिए, आपको अपनी डेटा लेयर में Compose डिपेंडेंसी जोड़नी होगी. हालांकि, यह सिर्फ़ Compose रनटाइम के लिए डिपेंडेंसी है, न कि
- अपने यूज़र इंटरफ़ेस (यूआई) मॉड्यूल में, डेटा लेयर क्लास को यूज़र इंटरफ़ेस (यूआई) के हिसाब से रैपर क्लास में रैप करें.
यही समस्या तब भी आती है, जब बाहरी लाइब्रेरी का इस्तेमाल किया जाता है और वे Compose कंपाइलर का इस्तेमाल नहीं करती हैं.
हर कंपोज़ेबल को स्किप नहीं किया जाना चाहिए
स्टेबिलिटी से जुड़ी समस्याओं को ठीक करते समय, हर कंपोज़ेबल को स्किप किया जा सकने वाला बनाने की कोशिश न करें. इसके अलावा, हर पैरामीटर पर स्ट्रक्चरल इक्वालिटी लागू करने की कोशिश न करें. किसी कंपोज़ेबल को स्किप करने के लिए, कुछ बातों का ध्यान रखना ज़रूरी है: स्किप किए जा सकने वाले कंपोज़ेबल के हर पैरामीटर को Compose रनटाइम सेव करता है, ताकि उसे याद रखा जा सके. साथ ही, जब भी फ़ंक्शन को फिर से कंपोज़ किया जाता है, तो Compose को हर पैरामीटर की तुलना उसकी पिछली वैल्यू से करनी होती है. इससे यह तय किया जाता है कि उसे स्किप किया जाना चाहिए या नहीं. हर जगह स्किप करने की कोशिश करने से, समय से पहले ऑप्टिमाइज़ेशन हो सकता है. इससे, फ़ायदा होने के बजाय ज़्यादा नुकसान हो सकता है.
ऐसी कई स्थितियां होती हैं जहां स्ट्रक्चरल स्टेबिलिटी को लागू करने से कोई फ़ायदा नहीं होता. साथ ही, इससे परफ़ॉर्मेंस या रखरखाव में गिरावट आ सकती है. उदाहरण के लिए:
- ऐसा कंपोज़ेबल जिसे अक्सर या कभी भी फिर से कंपोज़ नहीं किया जाता.
- ऐसा कंपोज़ेबल जो सिर्फ़ स्किप किए जा सकने वाले कंपोज़ेबल को कॉल करता है.
- ऐसा कंपोज़ेबल जिसमें कई पैरामीटर या कलेक्शन मॉडल हों और जिसे लागू करने में ज़्यादा
.equals()खर्च हो. इस मामले में, किसी पैरामीटर में बदलाव हुआ है या नहीं, यह जांचने की लागत, सस्ते रीकंपोज़िशन की लागत से ज़्यादा हो सकती है.
सिर्फ़ रीकंपोज़िशन की संख्या पर ज़्यादा ध्यान न दें. कंपोज़ेबल को हल्का बनाएं, ताकि उन्हें आसानी से लागू किया जा सके. साथ ही, तेज़ी से बदलने वाली स्थिति को पढ़ने की प्रोसेस को लेआउट या ड्रॉ फ़ेज़ तक टालें. इसके अलावा, मानदंड की मदद से बदलावों की पुष्टि करें. जब किसी कंपोज़ेबल को स्किप किया जा सकता है, तो इससे थोड़ा ओवरहेड जुड़ जाता है. ऐसा हो सकता है कि यह ओवरहेड काम का न हो. अगर आपको लगता है कि कंपोज़ेबल को रीस्टार्ट या स्किप करने की सुविधा देने से, फ़ायदे से ज़्यादा नुकसान होगा, तो कंपोज़ेबल को रीस्टार्ट न किया जा सकने वाला या स्किप न किया जा सकने वाला के तौर पर एनोटेट किया जा सकता है.