قد تواجه بعض المشاكل الشائعة في 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 تحديد أنّ العنصر في الموضع 3 هو نفسه العنصر الذي كان في الموضع 2. بما أنّ أيًا من بيانات هذا العنصر لم يتغيّر، لن يضطر 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)، ضع أيضًا
الإرشادات التالية في الاعتبار:
- استخدام مفاتيح فريدة يمكن حفظها في حِزم: يجب أن تكون المفاتيح فريدة وحتمية
ويمكن حفظها في
Bundleعلى Android لأنّ التنسيقات الكسولة تستخدمrememberSaveableداخليًا. يُفضَّل استخدام الأنواع الأساسية، مثلIntأوLongأوString، وعدم استخدام فهارس القوائم أوhashCode()كمفاتيح. - تحديد
contentTypeدائمًا للقوائم غير المتجانسة: يمكنك تمريرcontentType = { it.type }إلىitemsأوitemsIndexedلكي تعيد أداة Compose استخدام خانات الإنشاء بين العناصر التي تشترك في البنية المرئية نفسها أثناء الانتقال للأعلى أو للأسفل. - استخدام
itemsIndexedبدلاً منindexOf: تجنَّب استدعاءlist.indexOf(item)داخل كتلةitems، ما يؤدي إلى تحويل التنسيق إلى فحصO(N²)، واستخدِمitemsIndexedبدلاً من ذلك. - رفع الحالة المشتركة وقراءات
CompositionLocal: يمكنك جمع مثيلاتFlowالمشتركة وقراءة قيمCompositionLocal(مثلLocalContext.currentأوLocalDensity.current) مرة واحدة في الدالة المركّبة الرئيسية أعلى التنسيق الكسول وتمرير القيم التي تم حلّها إلى الدوال المركّبة للعناصر. - استخدام
RowأوColumnللقوائم الصغيرة المحدودة: بالنسبة إلى المجموعات الثابتة أو المحدودة الصغيرة (مثل عنصر واحد إلى 10 عناصر)، فإنّ استخدامRowأوColumnالعادي معforEachيتجنّب المحاكاة الافتراضية الكسولة والتحميل المُسبق الزائد.
استخدام computedStateOf أو derivedStateOf للحدّ من عمليات إعادة الإنشاء
من مخاطر استخدام الحالة في العناصر المركّبة أنّه في حال تغيُّر الحالة بسرعة، قد تتم إعادة إنشاء واجهة المستخدم أكثر من اللازم. على سبيل المثال، لنفترض أنّك تعرض قائمة قابلة للتمرير. يمكنك فحص حالة القائمة لمعرفة العنصر الأول المرئي في القائمة:
val listState = rememberLazyListState() LazyColumn(state = listState) { // ... } val showButton = listState.firstVisibleItemIndex > 0 AnimatedVisibility(visible = showButton) { ScrollToTopButton() }
المشكلة هنا هي أنّه إذا انتقل المستخدم في القائمة، يتغيّر listState باستمرار أثناء سحب المستخدم إصبعه. هذا يعني أن القائمة يتم
إعادة تكوينها باستمرار. ومع ذلك، لست بحاجة إلى إعادة إنشائها كثيرًا،
بل عليك إعادة إنشائها فقط عندما يظهر عنصر جديد في الأعلى. لذا،
هذا يتطلب الكثير من العمليات الحسابية الإضافية، ما يجعل أداء واجهة المستخدم سيئًا.
الحلّ هو استخدام الحالة المشتقة (أو computedStateOf في
Compose Runtime 1.13 والإصدارات الأحدث). تتيح لك الحالة المشتقة إخبار 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عند كل تغيير في الحالة ويمنع التخطّي.- دوال getter البسيطة في Kotlin (
get()) وعمليات قراءة السمات المباشرة: يُفضَّل استخدام دالة getter مخصّصة بسيطة للتعبيرات المنطقية البسيطة التي لا تتطلّب تقليل عمليات الإبطال، وقراءة مجموعة.sizeمباشرةً بدلاً من تضمينها فيderivedStateOf. تجنَّب استخدامproduceStateمعsnapshotFlowلعمليات اشتقاق الحالة المتزامنة، لأنّ ذلك يضيف عبئًا غير ضروري على إرسال الكوروتين.
تأجيل عمليات القراءة لأطول فترة ممكنة
عند تحديد مشكلة في الأداء، يمكن أن يساعد تأجيل قراءة الحالة. سيضمن تأجيل قراءة الحالة أن يعيد Compose تشغيل الحد الأدنى من الرموز البرمجية الممكنة عند إعادة الإنشاء. على سبيل المثال، إذا كانت واجهة المستخدم تتضمّن حالة تم رفعها إلى مستوى أعلى في شجرة الدوال المركّبة وقرأت الحالة في دالة مركّبة فرعية، يمكنك تضمين قراءة الحالة في دالة lambda. يؤدي القيام بذلك إلى حدوث القراءة فقط عند الحاجة إليها فعليًا. للاطّلاع على مرجع، يمكنك مراجعة عملية التنفيذ في نموذج تطبيق Jetsnack الذي يطبّق تأثيرًا يشبه شريط الأدوات القابل للتصغير على شاشة التفاصيل. لفهم سبب نجاح هذه الطريقة، يمكنك الاطّلاع على منشور المدوّنة Jetpack Compose: تصحيح أخطاء إعادة الإنشاء.
لتحقيق هذا التأثير، تحتاج الدالة المركّبة 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)، والذي يأخذ
الإزاحة كمعلمة. من خلال التبديل إلى إصدار lambda من المُعدِّل،
يمكنك التأكّد من أنّ الدالة تقرأ حالة الانتقال في مرحلة التنسيق. نتيجةً لذلك، عندما تتغيّر حالة الانتقال، يمكن لـ Compose تخطّي مرحلة الإنشاء بالكامل والانتقال مباشرةً إلى مرحلة التنسيق. عند تمرير متغيّرات الحالة التي تتغيّر بشكل متكرّر إلى المُعدِّلات، يجب استخدام إصدارات Lambda من المُعدِّلات كلما أمكن ذلك.
إليك مثال آخر على هذا النهج. لم يتم تحسين هذا الرمز بعد:
// Here, assume animateColorBetween() is a function that swaps between // two colors val color by animateColorBetween(Color.Cyan, Color.Magenta) Box( Modifier .fillMaxSize() .background(color) )
في هذا المثال، يتغيّر لون خلفية المربّع بسرعة بين لونَين. وبالتالي، تتغيّر هذه الحالة بشكل متكرّر. بعد ذلك، تقرأ الدالة المركّبة هذه الحالة في معدِّل الخلفية. نتيجةً لذلك، يجب إعادة إنشاء المربّع في كل إطار، لأنّ اللون يتغيّر في كل إطار.
لتحسين ذلك، استخدِم معدِّل يستند إلى دالة lambda، وفي هذه الحالة، 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 تركيب هذه الدالة المركّبة، ويرى قراءة حالة قديمة، وبالتالي يجدول إعادة تركيب أخرى.
يمكنك تجنُّب عمليات الكتابة العكسية تمامًا من خلال عدم الكتابة إلى الحالة في
الإنشاء. إذا كان ذلك ممكنًا، اكتب دائمًا في الحالة استجابةً لحدث
وفي دالة lambda كما في مثال onClick السابق.
للحصول على دليل مفصّل حول عمليات الكتابة العكسية وحلقات إعادة الإنشاء وتنسيق المراحل، يُرجى الاطّلاع على عمليات الكتابة العكسية في Compose.
مراجع إضافية
- دليل أداء التطبيقات: تعرَّف على أفضل الممارسات والمكتبات والأدوات لتحسين الأداء على Android.
- فحص الأداء: لفحص أداء التطبيق
- قياس الأداء: يمكنك قياس أداء التطبيق.
- بدء تشغيل التطبيق: يمكنك تحسين بدء تشغيل التطبيق.
- الملفات الشخصية للمرجع: يمكنك التعرّف على الملفات الشخصية للمرجع.
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- الحالة وJetpack Compose
- عناصر تعديل الرسومات
- التفكير في Compose