قد تواجه أخطاء شائعة في 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 أنّ العنصر الذي يظهر الآن في الموضع 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) } } }
استخدام 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 للحد الأدنى من الرموز البرمجية الممكنة عند إعادة التركيب. على سبيل المثال، إذا كانت واجهة المستخدم الخاصة بك تتضمّن حالة تم نقلها إلى أعلى شجرة الدوال المركّبة، وقرأت الحالة في دالة مركّبة فرعية، يمكنك تضمين قراءة الحالة في دالة lambda. يؤدي ذلك إلى حدوث القراءة فقط عند الحاجة إليها. للحصول على مرجع، اطّلِع على عملية التنفيذ في نموذج تطبيق Jetsnack. ينفّذ 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 نطاق إعادة التركيب الخاص بالعنصر الرئيسي الأقرب. في هذه الحالة، يكون النطاق الأقرب هو SnackDetailComposable. يُرجى العِلم أنّ 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) } )
تجنُّب عمليات الكتابة السابقة
تستند ميزة "الكتابة الذكية" إلى افتراض أساسي وهو أنّك لن تكتب أبدًا إلى حالة تمت قراءتها من قبل. عند إجراء ذلك، يُطلق عليه اسم الكتابة للخلف، ويمكن أن يؤدي إلى إعادة التركيب في كل إطار بشكل لا نهائي.
تعرض الدالة المركّبة التالية مثالاً على هذا النوع من الأخطاء.
@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 يعيد تركيب هذا العنصر القابل للإنشاء، ويرى قراءة حالة قديمة، وبالتالي يجدول عملية إعادة تركيب أخرى.
يمكنك تجنُّب عمليات الكتابة السابقة تمامًا من خلال عدم الكتابة إلى الحالة في
Composition. إذا أمكن ذلك، اكتب دائمًا حالة استجابةً لحدث
وفي تعبير lambda كما في المثال onClick السابق.
للحصول على دليل تفصيلي حول عمليات الكتابة المتوافقة مع الإصدارات القديمة وحلقات إعادة الإنشاء وتنسيق المراحل، يُرجى الاطّلاع على عمليات الكتابة المتوافقة مع الإصدارات القديمة في Compose.
مراجع إضافية
- دليل أداء التطبيقات: يمكنك الاطّلاع على أفضل الممارسات والمكتبات والأدوات لتحسين الأداء على Android.
- فحص الأداء: لفحص أداء التطبيق
- قياس الأداء: قياس أداء التطبيق.
- بدء تشغيل التطبيق: تحسين عملية بدء تشغيل التطبيق
- ملفات تعريف المرجع: يمكنك التعرّف على ملفات تعريف المرجع.
مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون JavaScript غير مفعّلة
- الحالة وJetpack Compose
- عناصر تعديل الرسومات
- التفكير في Compose