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

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

يمكنك استخدام "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 تأثيرًا يشبه شريط الأدوات القابل للتصغير على شاشة التفاصيل. لفهم سبب عمل هذه التقنية، اطّلِع على مشاركة المدونة: تصحيح الأخطاء في إعادة الإنشاء.

لتحقيق هذا التأثير، يجب أن يتعرّف عنصر 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 بالكامل.

وهذا تحسن جيد، ولكن يمكنك القيام بعمل أفضل! يجب أن تشعر بالريبة إذا كنت تتسبب في إعادة التركيب فقط لإعادة تنسيق أو إعادة رسم Composable. في هذه الحالة، كل ما تفعله هو تغيير إزاحة 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)
)

هنا، يتم تبديل لون خلفية المربع بسرعة بين لونين. وبالتالي تتغير هذه الحالة بشكل متكرر جدًا. بعد ذلك، يقرأ العنصر القابل للتركيب هذه الحالة في معدِّل الخلفية. نتيجةً لذلك، يجب إعادة تركيب المربع على كل إطار، لأن اللون يتغير على كل إطار.

لتحسين ذلك، يمكننا استخدام مُعدِّل مستند إلى لامدا، في هذه الحالة، 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 السابق.