Jetpack Compose هي مجموعة أدوات حديثة لواجهة المستخدم التعريفية في Android. تسهّل أداة Compose كتابة واجهة مستخدم تطبيقك وصيانتها من خلال توفير واجهة برمجة تطبيقات تعريفية تتيح لك عرض واجهة مستخدم تطبيقك بدون الحاجة إلى تغيير طرق العرض في الواجهة الأمامية بشكل إلزامي. تحتاج هذه المصطلحات إلى بعض الشرح، ولكن الآثار المترتبة عليها مهمة لتصميم تطبيقك.
نموذج البرمجة التعريفية
في السابق، كان من الممكن تمثيل هيكلية طرق العرض في Android على شكل شجرة من عناصر واجهة المستخدم. عندما تتغير حالة التطبيق بسبب تفاعلات المستخدم، يجب تحديث التسلسل الهرمي لواجهة المستخدم لعرض البيانات الحالية.
الطريقة الأكثر شيوعًا لتعديل واجهة المستخدم هي التنقّل في الشجرة باستخدام دوال مثل
findViewById()، وتغيير
العُقد من خلال استدعاء طرق مثل button.setText(String) أو
container.addChild(View) أو img.setImageBitmap(Bitmap). تغيّر هذه الطرق
الحالة الداخلية لعنصر واجهة المستخدم.
تؤدي معالجة المشاهدات يدويًا إلى زيادة احتمال حدوث أخطاء. إذا تم عرض جزء من البيانات في أماكن متعددة، فقد تنسى تحديث أحد طرق العرض التي تعرضه. يمكن أن يؤدي ذلك أيضًا إلى حالات غير قانونية، عندما يتعارض تحديثان بطريقة غير متوقعة. على سبيل المثال، قد يحاول تحديث ما ضبط قيمة عقدة تمت إزالتها للتو من واجهة المستخدم. بشكل عام، يزداد تعقيد صيانة البرامج مع زيادة عدد طرق العرض التي تتطلب التحديث.
على مدار السنوات القليلة الماضية، بدأ المجال بأكمله في التحول إلى نموذج واجهة مستخدم تعريفية، مما يبسط إنشاء واجهات المستخدم وتحديثها.
تعمل هذه التقنية من خلال إعادة إنشاء الشاشة بأكملها من البداية، ثم تطبيق التغييرات اللازمة فقط. يتجنّب هذا الأسلوب تعقيد التحديث اليدوي لهيكلية طرق العرض ذات الحالة. Compose هو إطار عمل لواجهة المستخدم التعريفية.
من بين التحديات التي تواجه إعادة إنشاء الشاشة بأكملها أنّها قد تكون مكلفة من حيث الوقت وقوة الحوسبة واستخدام البطارية. ولتقليل هذه التكلفة، يختار Compose بذكاء أجزاء واجهة المستخدم التي يجب إعادة رسمها في أي وقت. ويؤثر ذلك في كيفية تصميم مكوّنات واجهة المستخدم، كما هو موضّح في إعادة الإنشاء.
مثال على دالة مركّبة
باستخدام Compose، يمكنك إنشاء واجهة المستخدم من خلال تحديد مجموعة من الدوال القابلة للإنشاء التي تستقبل البيانات وتُخرج عناصر في واجهة المستخدم. على سبيل المثال،
هناك تطبيق مصغّر Greeting، يأخذ String ويُخرج تطبيقًا مصغّرًا Text
يعرض رسالة ترحيب.
بعض الأمور الجديرة بالملاحظة حول هذه الدالة:
- التعليق التوضيحي: يتم وضع التعليق التوضيحي
@Composableعلى الدالة. يجب أن تحتوي جميع الدوال المركّبة على هذا التعليق التوضيحي. يُعلم هذا التعليق التوضيحي برنامج تجميع Compose بأنّ هذه الدالة تهدف إلى تحويل البيانات إلى واجهة مستخدم. - إدخال البيانات: تأخذ الدالة البيانات. يمكن أن تقبل الدوال المركّبة المَعلمات، ما يتيح لمنطق التطبيق وصف واجهة المستخدم. في هذه الحالة،
يقبل التطبيق المصغّر
Stringحتى يتمكّن من الترحيب بالمستخدم بالاسم. - عرض واجهة المستخدم: تعرض الدالة نصًا في واجهة المستخدم. ويتم ذلك من خلال استدعاء الدالة المركّبة
Text()التي تنشئ عنصر النص في واجهة المستخدم. تُصدر الدوال المركّبة هيكلية واجهة المستخدم من خلال استدعاء دوال مركّبة أخرى. - ما مِن قيمة معروضة: لا تعرض الدالة أي قيمة. لا تحتاج دوال Compose التي تُصدر واجهة المستخدم إلى عرض أي شيء، لأنّها تصف حالة الشاشة المستهدَفة بدلاً من إنشاء تطبيقات مصغّرة لواجهة المستخدم.
الخصائص: هذه الدالة سريعة ومتكررة وخالية من الآثار الجانبية.
- تعمل الدالة بالطريقة نفسها عند استدعائها عدة مرات باستخدام
الوسيطة نفسها، ولا تستخدم قيمًا أخرى مثل المتغيرات العمومية
أو الاستدعاءات إلى
random(). - تصف الدالة واجهة المستخدم بدون أي تأثيرات جانبية، مثل تعديل الخصائص أو المتغيرات العمومية.
بشكل عام، يجب كتابة جميع الدوال المركّبة باستخدام هذه السمات، للأسباب التي تمت مناقشتها في إعادة التركيب.
- تعمل الدالة بالطريقة نفسها عند استدعائها عدة مرات باستخدام
الوسيطة نفسها، ولا تستخدم قيمًا أخرى مثل المتغيرات العمومية
أو الاستدعاءات إلى
التغيير الجذري في النماذج التعريفية
باستخدام العديد من مجموعات أدوات واجهة المستخدم الإلزامية والموجّهة للكائنات، يمكنك تهيئة واجهة المستخدم من خلال إنشاء مثيل لشجرة من التطبيقات المصغّرة. يمكنك إجراء ذلك غالبًا من خلال تضخيم ملف تنسيق XML. تحتفظ كل أداة بحالتها الداخلية، وتكشف عن طرق getter وsetter التي تتيح لمنطق التطبيق التفاعل مع الأداة.
في النهج التعريفي لـ Compose، تكون التطبيقات المصغّرة عديمة الحالة نسبيًا ولا
تعرض دوال setter أو getter. في الواقع، لا يتم عرض التطبيقات المصغّرة كعناصر.
يمكنك تعديل واجهة المستخدم من خلال استدعاء الدالة المركّبة نفسها باستخدام وسيطات مختلفة. ويسهّل ذلك توفير الحالة للأنماط المعمارية، مثل ViewModel، كما هو موضّح في دليل بنية التطبيق. بعد ذلك، تكون الدوال المركّبة مسؤولة عن تحويل حالة التطبيق الحالية إلى واجهة مستخدم في كل مرة يتم فيها تعديل البيانات القابلة للتتبّع.
عندما يتفاعل المستخدم مع واجهة المستخدم، تُنشئ واجهة المستخدم أحداثًا مثل onClick.
يجب أن تُرسل هذه الأحداث إشعارًا إلى منطق التطبيق، والذي يمكنه بعد ذلك تغيير حالة التطبيق.
عندما تتغيّر الحالة، يتم استدعاء الدوال المركّبة مرة أخرى باستخدام البيانات الجديدة. يؤدي ذلك إلى إعادة رسم عناصر واجهة المستخدم، وتُعرف هذه العملية باسم
إعادة التركيب.
المحتوى الديناميكي
بما أنّ الدوال المركّبة مكتوبة بلغة Kotlin بدلاً من XML، يمكن أن تكون ديناميكية مثل أي رمز برمجي آخر بلغة Kotlin. على سبيل المثال، لنفترض أنّك تريد إنشاء واجهة مستخدم تُرسل رسالة ترحيب إلى قائمة من المستخدمين:
@Composable fun Greeting(names: List<String>) { for (name in names) { Text("Hello $name") } }
تأخذ هذه الدالة قائمة بالأسماء وتنشئ تحية لكل مستخدم.
يمكن أن تكون الدوال المركّبة معقّدة للغاية. يمكنك استخدام عبارات if لتحديد ما إذا كنت تريد عرض عنصر معيّن في واجهة المستخدم. يمكنك استخدام التكرارات الحلقية. يمكنك
استدعاء الدوال المساعِدة. لديك المرونة الكاملة للغة الأساسية.
وتُعدّ هذه الإمكانات والمرونة من المزايا الرئيسية لـ Jetpack Compose.
إعادة التكوين
في نموذج واجهة المستخدم الإلزامية، لتغيير تطبيق مصغّر، يمكنك استدعاء دالة setter في التطبيق المصغّر لتغيير حالته الداخلية. في 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) } }
يتناول هذا المستند عددًا من النقاط التي يجب الانتباه إليها عند استخدام 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) })) }
قد يكون كل نطاق من هذه النطاقات هو الشيء الوحيد الذي يتم تنفيذه أثناء إعادة الإنشاء.
قد يتخطى Compose إلى دالة lambda Column بدون تنفيذ أي من الدوال الرئيسية
عند تغيير header. وعند تنفيذ Column، قد تختار Compose
تخطّي عناصر LazyColumn إذا لم يتغيّر names.
مرة أخرى، يجب أن تكون جميع الدوال المركّبة أو دوال lambda خالية من التأثيرات الجانبية. عندما تحتاج إلى تنفيذ تأثير جانبي، يمكنك تشغيله من خلال دالة رد اتصال.
إعادة التركيب متفائلة
تبدأ إعادة التركيب عندما يرى Compose أنّ مَعلمات دالة مركّبة قد تكون تغيّرت. تكون إعادة التركيب متفائلة، ما يعني أنّ Compose يتوقع الانتهاء من إعادة التركيب قبل تغيير المَعلمات مرة أخرى. إذا تغيّرت مَعلمة قبل انتهاء عملية إعادة الإنشاء، قد يلغي Compose عملية إعادة الإنشاء ويعيد تشغيلها باستخدام المَعلمة الجديدة.
عند إلغاء إعادة الإنشاء، يتجاهل Compose شجرة واجهة المستخدم من إعادة الإنشاء. إذا كانت لديك أي آثار جانبية تعتمد على واجهة المستخدم التي يتم عرضها، سيتم تطبيق الآثار الجانبية حتى إذا تم إلغاء التركيب. وقد يؤدي ذلك إلى عدم اتساق حالة التطبيق.
يُرجى التحقّق من أنّ جميع الدوال المركّبة ودوال lambda لا تتأثر بالعمليات المتكررة ولا تتضمّن أي تأثيرات جانبية، وذلك للتعامل مع إعادة التركيب المتفائلة.
قد يتم تشغيل الدوال المركّبة بشكل متكرر
في بعض الحالات، قد يتم تشغيل دالة مركّبة لكل إطار من إطارات الصور المتحركة في واجهة المستخدم. إذا كانت الدالة تُجري عمليات مكلِّفة، مثل القراءة من مساحة التخزين على الجهاز، فقد تتسبّب في حدوث خلل في واجهة المستخدم.
على سبيل المثال، إذا حاول التطبيق المصغّر قراءة إعدادات الجهاز، من المحتمل أن يقرأ هذه الإعدادات مئات المرات في الثانية، ما يؤدي إلى تأثيرات كارثية على أداء تطبيقك.
إذا كانت الدالة المركّبة تحتاج إلى بيانات، حدِّد مَعلمات لتلك البيانات. يمكنك
بعد ذلك نقل العمل المكلف إلى سلسلة محادثات أخرى، خارج التركيب، وتمرير
القيمة الناتجة إلى الدالة المركّبة كمَعلمة باستخدام
mutableStateOf أو LiveData.
يمكن تشغيل الدوال المركّبة بالتوازي
يمكن أن تحسِّن Compose عملية إعادة التكوين من خلال تشغيل الدوال المركّبة بشكل متوازٍ. سيسمح ذلك لـ Compose بالاستفادة من النوى المتعددة، وتشغيل الدوال المركّبة غير المعروضة على الشاشة بأولوية أقل.
يعني هذا التحسين أنّ الدالة المركّبة قد يتم تنفيذها ضمن مجموعة من سلاسل المحادثات في الخلفية.
إذا استدعت دالة مركّبة دالة أخرى في ViewModel،
قد يستدعي Compose هذه الدالة من عدة سلاسل محادثات في الوقت نفسه.
للتحقّق من أنّ تطبيقك يعمل بشكل صحيح، يجب ألا يكون لجميع الدوال المركّبة أي تأثيرات جانبية. بدلاً من ذلك، يمكنك تشغيل التأثيرات الجانبية من عمليات الاستدعاء، مثل
onClick التي يتم تنفيذها دائمًا في سلسلة واجهة المستخدم.
عند استدعاء دالة مركّبة، قد يتم الاستدعاء في سلسلة محادثات مختلفة عن المتصل. وهذا يعني أنّه يجب تجنُّب الرموز البرمجية التي تعدّل المتغيّرات في دالة مركّبة، لأنّ هذه الرموز ليست آمنة لسلسلة التعليمات البرمجية، ولأنّها تُعدّ تأثيرًا جانبيًا غير مسموح به لدالة 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 عمليات الكتابة من هذا النوع، لأنّ حظرها يتيح لإطار العمل تغيير سلاسل التعليمات لتنفيذ الدوال المركّبة.
يمكن تنفيذ الدوال المركّبة بأي ترتيب
إذا نظرت إلى الرمز البرمجي لدالة مركّبة، قد تفترض أنّ الرمز يتم تشغيله بالترتيب الذي يظهر به. لكن هذا ليس صحيحًا بالضرورة. إذا كانت دالة مركّبة تحتوي على طلبات لدوال مركّبة أخرى، قد يتم تشغيل هذه الدوال بأي ترتيب. يتيح Compose إمكانية التعرّف على أنّ بعض عناصر واجهة المستخدم لها أولوية أعلى من غيرها، ورسمها أولاً.
على سبيل المثال، لنفترض أن لديك رمزًا مثل هذا لرسم ثلاث شاشات في تخطيط علامة التبويب:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
قد يتم إجراء المكالمات إلى StartScreen وMiddleScreen وEndScreen بأي ترتيب. يعني ذلك أنّه لا يمكنك مثلاً ضبط StartScreen() على متغيّر عمومي (تأثير جانبي) واستخدام MiddleScreen() للاستفادة من هذا التغيير. بدلاً من ذلك، يجب أن تكون كل دالة من هذه الدوال قائمة بذاتها.
مزيد من المعلومات
لمزيد من المعلومات حول كيفية التفكير في Compose والدوال المركّبة، يُرجى الاطّلاع على المراجع الإضافية التالية.
الفيديوهات
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- Kotlin لـ Jetpack Compose
- الحالة وJetpack Compose
- بنية Jetpack Compose متعددة الطبقات