اتّباع أفضل الممارسات

قد تواجه بعض الصعوبات الشائعة في ميزة "الكتابة". قد تعطيك هذه الأخطاء تعليمات برمجية يبدو أنها تعمل بشكل جيد بما فيه الكفاية، ولكنها يمكن أن تؤذي أداء واجهة المستخدم لديك. اتبع أفضل الممارسات لتحسين تطبيقك على 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 إلى إلغاء صلاحية أقرب نطاق لإعادة الإنشاء الرئيسي. في هذه الحالة، يكون أقرب نطاق هو العنصر 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)
        }
)

تجنُّب الكتابة بالعكس

افتراض أساسي أنّ ميزة "الكتابة" لن تكتب مطلقًا للإشارة إلى أنّك قد سبقت قراءتها عند إجراء ذلك، يُطلق عليها كتابة للخلف ويمكن أن تؤدي إلى إعادة التركيب على كل إطار، إلى ما لا نهاية.

يعرض العنصر القابل للإنشاء التالي مثالاً على هذا النوع من الأخطاء.

@Composable
fun BadComposable() {
    var count by remember { mutableStateOf(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.

مراجع إضافية