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

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

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

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

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

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

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

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

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

لقطة شاشة لهاتف يعرض النص

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

إليك بعض الأشياء الجديرة بالملاحظة حول هذه الدالة:

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

  • تأخذ الدالة البيانات. يمكن للدوال القابلة للتعديل أن تقبل المعلمات، التي تسمح لمنطق التطبيق بوصف واجهة المستخدم. في هذه الحالة، تقبل الأداة String لكي تُحيي المستخدم باسمه.

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

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

  • هذه الدالة سريعة وغير فعالة وخالية من الآثار الجانبية.

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

    وبشكل عام، يجب كتابة جميع الدوال القابلة للإنشاء باستخدام هذه الخصائص، وذلك لأسباب تمت مناقشتها في مقالة إعادة الإنشاء.

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

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

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

صورة توضيحية لتدفق البيانات في واجهة المستخدم في Compose بدءًا من العناصر عالية المستوى وصولاً إلى العناصر الثانوية

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

يمكن لميزة "إنشاء الرسائل" تحسين عملية إعادة الإنشاء من خلال تنفيذ الدوال القابلة للإنشاء بشكل متوازٍ. يتيح ذلك لميزة "الكتابة" الاستفادة من نوى متعددة وتشغيل وظائف قابلة للإنشاء بدون أولوية أقل على الشاشة.

يعني هذا التحسين أنّ دالة قابلة للإنشاء قد يتم تنفيذها ضمن مجموعة من سلاسل المحادثات في الخلفية. إذا استدعت دالة قابلة للإنشاء دالة على 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
@Deprecated("Example with bug")
fun ListWithBug(myList: List<String>) {
    var items = 0

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

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

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

عندما تكون أجزاء من واجهة المستخدم غير صالحة، تبذل ميزة 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)
        Divider()

        // 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.

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

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

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

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

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

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

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

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

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

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

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

الفيديوهات الطويلة