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

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

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

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

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

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

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

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

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

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

الشكل 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. يستدعي Compose دالة lambda باستخدام الدالة Text مرة أخرى لعرض القيمة الجديدة، وتُسمى هذه العملية إعادة التركيب. ولا تتم إعادة تركيب الدوال الأخرى التي لا تعتمد على القيمة.

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

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

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

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

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

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

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

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

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

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

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

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

يمكن إعادة إنشاء كل دالة قابلة للإنشاء ودالة 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 تتوقع إنهاء عملية إعادة الإنشاء قبل أن تتغير المعلمات مرة أخرى. في حال حدوث تغيير في إحدى المَعلَمات قبل انتهاء عملية إعادة الإنشاء، قد يلغي Compose عملية إعادة الإنشاء ويعيد تشغيله باستخدام المَعلمة الجديدة.

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

تأكد من أن جميع الدوال القابلة للإنشاء والدوال lambdas ثابتة وخالية من الآثار الجانبية للتعامل مع إعادة التركيب المتفائل.

قد يتم تشغيل الدوال القابلة للإنشاء بشكلٍ متكرّر.

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

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

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

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

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

الفيديوهات