التفكير في الإنشاء

‫Jetpack Compose هي مجموعة أدوات حديثة لواجهة مستخدِم تعريفية لنظام التشغيل Android. يسهّل إطار عمل Compose كتابة واجهة مستخدم تطبيقك وصيانتها من خلال توفير واجهة برمجة تطبيقات تعريفية تسمح لك بعرض واجهة مستخدم تطبيقك بدون تغيير طرق عرض الواجهة الأمامية بشكل إلزامي. تحتاج هذه المصطلحات إلى بعض التفسير، ولكنّ الآثار المترتبة عليها مهمة ل تصميم تطبيقك.

منهج البرمجة التعريفية

في السابق، كان من الممكن تمثيل التسلسل الهرمي لعرض Android على شكل شجرة عناصر واجهة مستخدِم صغيرة. عندما تتغيّر حالة التطبيق بسبب تفاعلات المستخدِم، يجب تعديل التسلسل الهرمي لواجهة المستخدم لعرض البيانات الحالية. إنّ الطريقة الأكثر شيوعًا لتعديل واجهة المستخدم هي التنقّل في الشجرة باستخدام دوالّ مثل findViewById()، وتغيير العقد من خلال استدعاء طرق مثل button.setText(String) أو container.addChild(View) أو img.setImageBitmap(Bitmap). تؤدي هذه الطرق إلى تغيير الحالة الداخلية للتطبيق المصغّر.

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

على مدار السنوات العديدة الماضية، بدأت الصناعة بأكملها في الانتقال إلى نموذج واجهة مستخدم توضيحي، ما يسهّل إلى حد كبير عملية الهندسة المرتبطة بإنشاء واجهات المستخدم وتعديلها. تعمل هذه التقنية من خلال إعادة إنشاء الشاشة بالكامل من الصفر، ثم تطبيق التغيُّرات اللازمة فقط. ويتجنّب هذا الأسلوب تعقيد تعديل التدرّج الهرمي لعرض الذي يتضمّن حالة. ‫Compose هو إطار عمل لتصميم واجهة المستخدم.

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

دالة مركّبة بسيطة

باستخدام Compose، يمكنك إنشاء واجهة المستخدم من خلال تحديد مجموعة من الدوال القابلة للتجميع التي تتلقّى البيانات وتُنشئ عناصر واجهة المستخدم. من الأمثلة البسيطة تطبيق مصغّر Greeting يتلقّى String ويُرسِل تطبيق مصغّر Text يعرض رسالة ترحيب.

لقطة شاشة لهاتف يعرض النص "مرحبًا بك"، ورمز برمجي لدالّة Composable بسيطة تُنشئ واجهة مستخدم مماثلة

الشكل 1: دالة قابلة للتجميع بسيطة يتم تمريرها بالبيانات واستخدامها لمحاولة عرض تطبيق مصغّر نصي على الشاشة

في ما يلي بعض النقاط التي يجب أخذها في الاعتبار بشأن هذه الدالة:

  • تمّت إضافة تعليق توضيحي @Composable إلى الدالة. يجب أن تحتوي جميع الدوال المكوّنة على هذا التعليق التوضيحي، حيث يُعلم هذا التعليق التوضيحي مُجمِّع Compose بأنّ هذه الدالة مخصّصة لتحويل البيانات إلى واجهة مستخدم.

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

  • تعرِض الدالة النص في واجهة المستخدم. ويتم ذلك من خلال استدعاء Text() دالة قابلة للتجميع، والتي تنشئ في الواقع عنصر واجهة المستخدم النصي. تُنشئ الدوال المركّبة هيكل واجهة المستخدم من خلال استدعاء دوال مركّبة أخرى.

  • لا تُرجع الدالة أيّ قيمة. لا تحتاج وظائف الإنشاء التي تُنشئ واجهة المستخدم إلى عرض أي شيء، لأنّها تصف حالة الشاشة المطلوبة بدلاً من إنشاء التطبيقات المصغّرة لواجهة المستخدم.

  • هذه الدالة سريعة، متساوية، وليست لها تأثيرات جانبية.

    • تتصرّف الدالة بالطريقة نفسها عند استدعائها عدة مرات باستخدام الوسيطة نفسها، ولا تستخدِم قيمًا أخرى، مثل المتغيّرات العامة أو طلبات الاستدعاء إلى random().
    • تصف الدالة واجهة المستخدم بدون أيّ تأثيرات جانبية، مثل تعديل السمات أو المتغيّرات الشاملة.

    بشكل عام، يجب كتابة جميع الدوالّ القابلة للتجميع باستخدام هذه السمات، وذلك للأسباب الموضّحة في إعادة التركيب.

تغيير المنهج التعريفي

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

في النهج التعريفي لـ Compose، تكون التطبيقات المصغّرة غير مرتبطة بحالة نسبيًا ولا تُعرِض وظائف ضبط أو الحصول. في الواقع، لا يتم عرض التطبيقات المصغّرة كعناصر. يمكنك تعديل واجهة المستخدم من خلال استدعاء الدالة القابلة للتجميع نفسها باستخدام واسطات مختلفة. يسهّل ذلك تقديم حالة للأنماط المعمارية، مثل ViewModel، كما هو موضّح في دليل بنية التطبيق. بعد ذلك، تكون العناصر القابلة للتجميع مسؤولة عن تحويل حالة التطبيق الحالية إلى واجهة مستخدم في كل مرة تتم فيها تعديل البيانات القابلة للتتبّع.

صورة توضيحية لتدفّق البيانات في واجهة مستخدم Compose، من العناصر ذات المستوى الأعلى إلى
العناصر
الفرعية

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

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

صورة توضيحية لكيفية استجابة عناصر واجهة المستخدم للتفاعل، من خلال بدء الأحداث
التي يعالجها منطق
التطبيق

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

المحتوى الديناميكي

بما أنّ الدوالّ القابلة للتجميع مكتوبة بلغة Kotlin بدلاً من XML، يمكن أن تكون ديناميكية مثل أي رمز Kotlin آخر. على سبيل المثال، لنفترض أنّك تريد إنشاء واجهة مستخدم ترحب بقائمة من المستخدمين:

@Composable
fun Greeting(names: List<String>) {
    for (name in names) {
        Text("Hello $name")
    }
}

تستقبل هذه الدالة قائمة بالأسماء وتُنشئ رسالة ترحيب لكل مستخدم. يمكن أن تكون الدوالّ القابلة للتجميع معقّدة جدًا. يمكنك استخدام عبارات if لتحديد ما إذا كنت تريد عرض عنصر واجهة مستخدم معيّن. يمكنك استخدام الحلقات. يمكنك استدعاء الدوال المساعِدة. يمكنك الاستفادة من المرونة الكاملة للغة الأساسية. تُعدّ هذه الإمكانات والمرونة من المزايا الرئيسية لواجهة Jetpack Compose.

إعادة التركيب

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

على سبيل المثال، فكِّر في هذه الدالة القابلة للتجميع التي تعرِض زرًا:

@Composable
fun ClickCounter(clicks: Int, onClick: () -> Unit) {
    Button(onClick = onClick) {
        Text("I've been clicked $clicks times")
    }
}

في كل مرة يتم فيها النقر على الزر، يعدّل المُتصل قيمة clicks. تستدعي دالة التركيب دالة LAMBDA مع دالة Text مرة أخرى لعرض القيمة الجديدة، ويُطلق على هذه العملية اسم إعادة التركيب. ولا تتم إعادة تركيب الدوال الأخرى التي لا تعتمد على القيمة.

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

إعادة التركيب هي عملية استدعاء الدوالّ القابلة للتجميع مرة أخرى عند تغيُّر المدخلات. ويحدث ذلك عند تغيير مدخلات الدالة. عند إعادة تركيب Compose استنادًا إلى مدخلات جديدة، لا تستدعي سوى الدوالّ أو الدوالّ اللامدا التي قد تغيّرت، وتتخطّى الباقي. من خلال تخطّي جميع الدوالّ أو الدوالّ اللامدا التي لم يتم تغيير مَعلماتها، يمكن لـ Compose إعادة الإنشاء بكفاءة.

لا تعتمد أبدًا على التأثيرات الجانبية الناتجة عن تنفيذ الدوالّ القابلة للتجميع، لأنّه قد يتم تخطّي إعادة تركيب الدوالّ. في حال إجراء ذلك، قد يواجه المستخدمون سلوكًا غريبًا وغير متوقّع في تطبيقك. ويُعدّ أي تغيير مرئيًا لبقية أجزاء تطبيقك من الآثار الجانبية. على سبيل المثال، جميع الإجراءات التالية هي آثار جانبية خطيرة:

  • الكتابة في سمة عنصر مشترَك
  • تعديل سمة قابلة للرصد في ViewModel
  • تعديل الإعدادات المفضّلة المشتركة

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

على سبيل المثال، تنشئ هذه التعليمة البرمجية عنصرًا قابلاً للتجميع لتعديل قيمة في SharedPreferences. يجب ألّا تقرأ العناصر القابلة للتجميع أو تكتب من الإعدادات المفضّلة المشترَكة نفسها. بدلاً من ذلك، تنقل هذه التعليمة البرمجية القراءة والكتابة إلى ViewModel في دالة معالجة متزامنة في الخلفية. يُرسِل منطق التطبيق القيمة الحالية باستخدام دالّة callback لبدء عملية التحديث.

@Composable
fun SharedPrefsToggle(
    text: String,
    value: Boolean,
    onValueChanged: (Boolean) -> Unit
) {
    Row {
        Text(text)
        Checkbox(checked = value, onCheckedChange = onValueChanged)
    }
}

يتناول هذا المستند عددًا من الأمور التي يجب أخذها في الاعتبار عند استخدام ميزة "الإنشاء":

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

ستتناول الأقسام التالية كيفية إنشاء دوال قابلة للتجميع لدعم إعادة التركيب. في جميع الحالات، من أفضل الممارسات إبقاء الدوالّ القابلة للتجميع سريعة وبدون تكرار وبدون تأثيرات جانبية.

تخطّي أكبر قدر ممكن من عمليات إعادة التركيب

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

قد تعيد كل دالة قابلة للتجميع ودالة LAMBDA تكوين نفسها بنفسها. في ما يلي مثال يوضّح كيف يمكن أن تتخطّى إعادة التركيب بعض العناصر عند عرض قائمة:

/**
 * Display a list of names the user can click with a header
 */
@Composable
fun NamePicker(
    header: String,
    names: List<String>,
    onNameClicked: (String) -> Unit
) {
    Column {
        // this will recompose when [header] changes, but not when [names] changes
        Text(header, style = MaterialTheme.typography.bodyLarge)
        HorizontalDivider()

        // LazyColumn is the Compose version of a RecyclerView.
        // The lambda passed to items() is similar to a RecyclerView.ViewHolder.
        LazyColumn {
            items(names) { name ->
                // When an item's [name] updates, the adapter for that item
                // will recompose. This will not recompose when [header] changes
                NamePickerItem(name, onNameClicked)
            }
        }
    }
}

/**
 * Display a single name the user can click.
 */
@Composable
private fun NamePickerItem(name: String, onClicked: (String) -> Unit) {
    Text(name, Modifier.clickable(onClick = { onClicked(name) }))
}

قد يكون كل نطاق من هذه النطاقات هو الإجراء الوحيد الذي يتم تنفيذه أثناء إعادة التركيب. قد يتخطّى التركيب دالة lambda‏ Column بدون تنفيذ أيّ من الدوالّ الرئيسية عند تغيير header. وعند تنفيذ Column، قد يختار Compose تخطّي عناصر LazyColumn إذا لم يتغيّر names.

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

إعادة التركيب متشائمة

تبدأ عملية إعادة التركيب عندما يظنّ تطبيق Compose أنّه قد تغيّرت مَعلمات العنصر القابل للتركيب. تكون إعادة التركيب متفائلة، ما يعني أنّ Compose تتوقّع إنهاء إعادة التركيب قبل تغيير المَعلمات مرة أخرى. إذاتغيّرت إحدى المَعلمات قبل انتهاء عملية إعادة التركيب، قد تلغي ميزة "إنشاء" عملية إعادة التركيب وتعيد تشغيلها باستخدام المَعلمة الجديدة.

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

تأكَّد من أنّ جميع الدوالّ القابلة للتجميع ودوالّ Lambda لا تؤدي إلى تكرار الإجراء ولا تتسبّب في أي آثار جانبية لمعالجة إعادة التركيب التفاؤلي.

قد يتم تشغيل الدوالّ القابلة للتجميع بشكلٍ متكرّر.

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

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

إذا كانت الدالة القابلة للتجميع تحتاج إلى بيانات، يجب أن تحدِّد مَعلمات لتلك البيانات. يمكنك بعد ذلك نقل العمل المكثّف إلى سلسلة محادثات أخرى خارج عملية الcomposing، ونقل البيانات إلى Compose باستخدام mutableStateOf أو LiveData.

يمكن تشغيل الدوالّ القابلة للتجميع بالتوازي

يمكن أن تحسِّن ميزة "الإنشاء" عملية إعادة الإنشاء من خلال تشغيل الدوالّ القابلة للتجميع في الموازاة. سيتيح ذلك لتطبيق Compose الاستفادة من النوى المتعددة وتنفيذ الدوال القابلة للتجميع التي لا تظهر على الشاشة بأولوية أقل.

سيؤدي هذا التحسين إلى تنفيذ دالة قابلة للتجميع ضمن مجموعة من سلاسل المهام التي تعمل في الخلفية. إذا كانت دالة مركّبة تستدعي دالة في ViewModel، قد تستدعي دالة Compose تلك الدالة من عدة سلاسل محادثات في الوقت نفسه.

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

عند استدعاء دالة قابلة للتجميع، قد يحدث الاستدعاء في سلسلتَي رسائل مختلفتَين عن سلسلة رسائل المُتصل. وهذا يعني أنّه يجب تجنُّب الرمز البرمجي الذي يعدّل المتغيّرات في دالة lambda قابلة للتركيب، وذلك لأنّ هذا الرمز البرمجي غير آمن لعمليات المعالجة المتعدّدة، ولأنّه ينتج عنه تأثير جانبي غير مسموح به لدالة lambda القابلة للتركيب.

في ما يلي مثال يعرض عنصرًا قابلاً للتجميع يعرض قائمة وعدد عناصرها:

@Composable
fun ListComposable(myList: List<String>) {
    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Text("Item: $item")
            }
        }
        Text("Count: ${myList.size}")
    }
}

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

@Composable
fun ListWithBug(myList: List<String>) {
    var items = 0

    Row(horizontalArrangement = Arrangement.SpaceBetween) {
        Column {
            for (item in myList) {
                Card {
                    Text("Item: $item")
                    items++ // Avoid! Side-effect of the column recomposing.
                }
            }
        }
        Text("Count: $items")
    }
}

في هذا المثال، يتم تعديل items مع كل إعادة تركيب. ويمكن أن يكون ذلك عند كل لقطة من صورة متحركة أو عند تعديل القائمة. وفي كلتا الحالتَين، ستعرض واجهة المستخدم عددًا غير صحيح. ولهذا السبب، لا تتوفّر عمليات الكتابة هذه في Compose. ومن خلال حظر عمليات الكتابة هذه، نسمح للإطار الأساسي بتغيير المواضيع لتنفيذ الدوالّ lambda القابلة للتركيب.

يمكن تنفيذ الدوالّ القابلة للتجميع بأي ترتيب.

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

على سبيل المثال، لنفترض أنّ لديك رمزًا برمجيًا مثل هذا لرسم ثلاث شاشات في تنسيق علامة تبويب:

@Composable
fun ButtonRow() {
    MyFancyNavigation {
        StartScreen()
        MiddleScreen()
        EndScreen()
    }
}

قد يتم إجراء المكالمات إلى StartScreen وMiddleScreen وEndScreen بأي ترتيب. وهذا يعني أنّه لا يمكنك، على سبيل المثال، ضبط StartScreen() لبعض المتغيّرات العميقة (تأثير جانبي) والاستفادة من MiddleScreen() من هذا التغيير. بدلاً من ذلك، يجب أن تكون كلّ وظيفة من هذه الدوالّ مكتفية ذاتيًا.

مزيد من المعلومات

لمعرفة المزيد حول كيفية التفكير في Compose والدوالّ القابلة للتجميع، اطّلِع على الموارد الإضافية التالية.

الفيديوهات