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

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

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

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

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

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

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

دالة بسيطة قابلة للإنشاء

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

لقطة شاشة لهاتف يعرض النص "Hello World" ورمز دالة Composable البسيطة التي تنشئ واجهة المستخدم هذه

الشكل 1. دالة بسيطة قابلة للإنشاء يتم تمرير البيانات إليها وتستخدمها لعرض أداة نصية على الشاشة.

في ما يلي بعض الملاحظات الجديرة بالذكر حول هذه الدالة:

  • يتم إضافة التعليق التوضيحي @Composable إلى الدالة. يجب أن تتضمّن جميع دوال 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 مرة أخرى لعرض القيمة الجديدة، وتُعرف هذه العملية باسم إعادة التركيب. ولا تتم إعادة إنشاء الدوال الأخرى التي لا تعتمد على القيمة.

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

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

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

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

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

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

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

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

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

ستتناول الأقسام التالية كيفية إنشاء دوال قابلة للإنشاء لدعم إعادة التركيب. في كل الحالات، أفضل ممارسة هي الحفاظ على سرعة وظائفك القابلة للإنشاء، وأن تكون متكررة، وخالية من الآثار الجانبية.

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

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

قد تتم إعادة إنشاء كل دالة قابلة للإنشاء ودالة 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) }))
}

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

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

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

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

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

تأكَّد من أنّ جميع الدوال القابلة للإنشاء وعبارات lambda متكرّرة وغير مرتبطة بأي آثار جانبية للتعامل مع إعادة التركيب المتفائلة.

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

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

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

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

يمكن تنفيذ الدوال البرمجية القابلة للإنشاء بأي ترتيب

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

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

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

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

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

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

الفيديوهات