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

‫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 استنادًا إلى مدخلات جديدة، لا تستدعي سوى الدوالّ أو الدوالّ اللامدا التي قد تغيّرت، وتتخطّى الباقي. من خلال تخطي جميع الدوال أو دالة lambdas التي لم تُغيّر مَعلَمات، يمكن لميزة Compose إعادة التركيب بكفاءة.

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

  • الكتابة في سمة عنصر مشترَك
  • جارٍ تعديل عنصر ملحوظ في ViewModel
  • تعديل الإعدادات المفضّلة المشتركة

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

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

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

يناقش هذا المستند عددًا من الأشياء التي يجب أن تكون على دراية بها عند استخدام Compose:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

الفيديوهات