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