Jetpack Compose هي مجموعة أدوات حديثة لواجهة المستخدم التعريفية في Android. يسهّل إطار عمل Compose كتابة واجهة مستخدم تطبيقك وصيانتها من خلال توفير واجهة برمجة تطبيقات تعريفية تسمح لك بعرض واجهة مستخدم تطبيقك بدون تغيير طرق عرض الواجهة الأمامية بشكل إلزامي. تحتاج هذه المصطلحات إلى بعض التفسير، ولكنّ الآثار المترتبة عليها مهمة ل تصميم تطبيقك.
منهج البرمجة التعريفية
في السابق، كان من الممكن تمثيل التسلسل الهرمي لعرض Android على شكل شجرة تطبيقات مصغّرة لواجهة المستخدم. عندما تتغيّر حالة التطبيق بسبب تفاعلات المستخدِم، يجب تعديل التسلسل الهرمي لواجهة المستخدم لعرض البيانات الحالية.
إنّ الطريقة الأكثر شيوعًا لتعديل واجهة المستخدم هي التنقّل في الشجرة باستخدام دوالّ مثل
findViewById()
، وتغيير
العقد من خلال استدعاء طرق مثل button.setText(String)
أو
container.addChild(View)
أو img.setImageBitmap(Bitmap)
. تؤدي هذه الطرق
إلى تغيير الحالة الداخلية للتطبيق المصغّر.
يؤدي التلاعب بالمشاهدات يدويًا إلى زيادة احتمالية حدوث أخطاء. إذا تم عرض قطعة من البيانات في أماكن متعدّدة، من السهل نسيان تعديل أحد ملفّات العرض التي تعرضها. ومن السهل أيضًا إنشاء حالات غير قانونية، عندما يتعارض تحديثان بطريقة غير متوقّعة. على سبيل المثال، قد يحاول أحد التعديلات ضبط قيمة لعقدة تمّت إزالتها للتو من واجهة المستخدم. بشكل عام، تزداد صعوبة صيانة البرامج مع زيادة عدد العروض التي تتطلّب التحديث.
على مدار السنوات العديدة الماضية، بدأت الصناعة بأكملها في الانتقال إلى نموذج واجهة مستخدم توضيحي، ما يسهّل بشكل كبير عملية الهندسة المرتبطة بإنشاء واجهات المستخدم وتعديلها. تعمل هذه التقنية من خلال إعادة إنشاء الشاشة بالكامل من الصفر، ثم تطبيق التغيُّرات اللازمة فقط. ويتجنّب هذا الأسلوب تعقيد تعديل التدرّج الهرمي لعرض الذي يتضمّن حالة. Compose هو إطار عمل لتصميم واجهة المستخدم.
يتمثل أحد التحديات في إعادة إنشاء الشاشة بالكامل في أنّه قد يكون مكلفًا من حيث الوقت وقوة الحوسبة واستخدام البطارية. للحدّ من هذه التكلفة، يختار تطبيق "الكتابة الذكية" أجزاء واجهة المستخدم التي يجب redrawn إعادة رسمها في أي وقت. ويؤدّي ذلك إلى بعض النتائج في كيفية تصميم مكونات واجهة المستخدم، كما هو موضّح في إعادة التركيب.
دالة مركّبة بسيطة
باستخدام Compose، يمكنك إنشاء واجهة المستخدم من خلال تحديد مجموعة من الدوال
القابلة للتجميع التي تتلقّى البيانات وتُنشئ عناصر واجهة المستخدم. من الأمثلة البسيطة
تطبيق مصغّر Greeting
يتلقّى String
ويُرسِل تطبيق مصغّر Text
يعرض رسالة ترحيب.
الشكل 1: دالة قابلة للتجميع بسيطة يتم تمريرها بالبيانات واستخدامها لمحاولة عرض تطبيق مصغّر نصي على الشاشة
في ما يلي بعض النقاط التي يجب أخذها في الاعتبار بشأن هذه الدالة:
تمّت إضافة تعليق توضيحي
@Composable
إلى الدالة. يجب أن تحتوي جميع الدوال المكوّنة على هذا التعليق التوضيحي، حيث يُعلم هذا التعليق التوضيحي مُجمِّع Compose بأنّ هذه الدالة مخصّصة لتحويل البيانات إلى واجهة مستخدم.تأخذ الدالة البيانات. يمكن أن تقبل الدوال القابلة للإنشاء مَعلمات، مما يسمح لمنطق التطبيق بوصف واجهة المستخدم. في هذه الحالة، يقبل التطبيق المصغّر
String
لكي يتمكّن من تحية المستخدم باسمه.تعرِض الدالة النص في واجهة المستخدم. ويتم ذلك من خلال استدعاء
Text()
دالة قابلة للتجميع، والتي تنشئ في الواقع عنصر واجهة المستخدم النصي. تُنشئ الدوال المركّبة هيكل واجهة المستخدم من خلال استدعاء دوال مركّبة أخرى.لا تُرجع الدالة أيّ قيمة. لا تحتاج وظائف الإنشاء التي تُنشئ واجهة المستخدم إلى عرض أي شيء، لأنّها تصف حالة الشاشة المطلوبة بدلاً من إنشاء التطبيقات المصغّرة لواجهة المستخدم.
هذه الدالة سريعة، متساوية، وليست لها تأثيرات جانبية.
- تتصرّف الدالة بالطريقة نفسها عند استدعائها عدة مرات باستخدام الوسيطة
نفسها، ولا تستخدِم قيمًا أخرى، مثل المتغيّرات العامة
أو طلبات البيانات إلى
random()
. - تصف الدالة واجهة المستخدم بدون أيّ تأثيرات جانبية، مثل تعديل السمات أو المتغيّرات الشاملة.
بشكل عام، يجب كتابة جميع الدوالّ القابلة للتجميع باستخدام هذه السمات، وذلك للأسباب الموضّحة في إعادة التركيب.
- تتصرّف الدالة بالطريقة نفسها عند استدعائها عدة مرات باستخدام الوسيطة
نفسها، ولا تستخدِم قيمًا أخرى، مثل المتغيّرات العامة
أو طلبات البيانات إلى
تغيير المنهج التعريفي
باستخدام العديد من حِزم أدوات واجهة المستخدم المستندة إلى العناصر، يمكنك بدء واجهة المستخدم من خلال إنشاء شجيرة من التطبيقات المصغّرة. وغالبًا ما يتم ذلك من خلال تضخيم ملف تنسيق XML. تحافظ كل أداة على حالتها الداخلية، وتوفّر طُرق الحصول على البيانات وتعديلها التي تسمح لمنطق التطبيق بالتفاعل مع الأداة.
في النهج التعريفي لـ Compose، تكون التطبيقات المصغّرة غير مرتبطة بحالة نسبيًا ولا تُعرِض
وظائف ضبط أو الحصول. في الواقع، لا يتم عرض التطبيقات المصغّرة كعناصر.
يمكنك تعديل واجهة المستخدم من خلال استدعاء الدالة القابلة للتجميع نفسها باستخدام واسطات مختلفة. يسهّل ذلك تقديم حالة للأنماط المعمارية، مثل
ViewModel
، كما هو موضّح في دليل بنية التطبيق. بعد ذلك، تكون العناصر القابلة للتجميع
مسؤولة عن تحويل حالة التطبيق الحالية إلى واجهة مستخدم في كل مرة تتم فيها تعديل البيانات القابلة للتتبّع.
الشكل 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 استنادًا إلى مدخلات جديدة، لا تستدعي سوى الدوالّ أو الدوالّ اللامدا التي قد تغيّرت، وتتخطّى الباقي. من خلال تخطّي جميع الدوالّ أو الدوالّ اللامدا التي لم يتم تغيير مَعلماتها، يمكن لـ Compose إعادة الإنشاء بكفاءة.
لا تعتمد أبدًا على التأثيرات الجانبية الناتجة عن تنفيذ الدوالّ القابلة للتجميع، لأنّه قد يتم تخطّي إعادة تركيب الدوالّ. في حال إجراء ذلك، قد يواجه المستخدمون سلوكًا غريبًا وغير متوقّع في تطبيقك. ويُعدّ أي تغيير مرئيًا لبقية أجزاء تطبيقك من الآثار الجانبية. على سبيل المثال، جميع الإجراءات التالية هي آثار جانبية خطيرة:
- الكتابة في سمة عنصر مشترَك
- تعديل سمة قابلة للرصد في
ViewModel
- تعديل الإعدادات المفضّلة المشتركة
قد تتم إعادة تنفيذ الدوال القابلة للتجميع في كل إطار، مثل عند عرض صورة متحركة. يجب أن تكون الدوالّ القابلة للتجميع سريعة لتجنُّب البطء أثناء عرض الصور المتحركة. إذا كنت بحاجة إلى تنفيذ عمليات مُكلّفة، مثل القراءة من الإعدادات المفضّلة المشتركة، يمكنك تنفيذها في دالة معالجة متزامنة في الخلفية وتمرير نتيجة قيمة إلى الدالة القابلة للتجميع كمَعلمة.
على سبيل المثال، تنشئ هذه التعليمة البرمجية عنصرًا قابلاً للتجميع لتعديل قيمة في
SharedPreferences
. يجب ألا يقرأ العنصر القابل للتجميع أو يكتب من الإعدادات المفضّلة المشترَكة نفسها. بدلاً من ذلك، تنقل هذه التعليمة البرمجية القراءة والكتابة إلى ViewModel
في دالة معالجة متعدّدة المهام في الخلفية. يُرسِل منطق التطبيق القيمة الحالية باستخدام دالّة callback لبدء عملية التحديث.
@Composable fun SharedPrefsToggle( text: String, value: Boolean, onValueChanged: (Boolean) -> Unit ) { Row { Text(text) Checkbox(checked = value, onCheckedChange = onValueChanged) } }
يتناول هذا المستند عددًا من الأمور التي يجب أخذها في الاعتبار عند استخدام ميزة "الإنشاء":
- تتخطّى إعادة التركيب أكبر عدد ممكن من الدوالّ القابلة للتجميع ودوالّ LAMBDA.
- إنّ إعادة التركيب متفائلة وقد يتم إلغاؤها.
- قد يتم تشغيل الدالة القابلة للتجميع بشكل متكرر جدًا، كل إطار من صورة متحركة.
- يمكن تنفيذ الدوالّ القابلة للتجميع بالتوازي.
- يمكن تنفيذ الدوالّ القابلة للتجميع بأي ترتيب.
ستتناول الأقسام التالية كيفية إنشاء دوال قابلة للتجميع لدعم إعادة التركيب. في جميع الحالات، من أفضل الممارسات إبقاء الدوالّ القابلة للتجميع سريعة وبدون تكرار وبدون تأثيرات جانبية.
تخطّي أكبر قدر ممكن من عمليات إعادة التركيب
عندما تكون أجزاء من واجهة المستخدم غير صالحة، تبذل ميزة "الإنشاء" قصارى جهدها لإعادة إنشاء الأجزاء التي تحتاج إلى تعديل فقط. وهذا يعني أنّه قد يتخطّى إعادة تشغيل ملف واحد قابل للتجميع من 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 تتوقّع إنهاء إعادة التركيب قبل تغيير المَعلمات مرة أخرى. إذاتغيّرت إحدى المَعلمات قبل انتهاء عملية إعادة التركيب، قد تلغي ميزة "الإنشاء" عملية إعادة التركيب وتعيد تشغيلها باستخدام المَعلمة الجديدة.
عند إلغاء إعادة التركيب، تتخلّص أداة الإنشاء من شجرة واجهة المستخدم من عملية إعادة التركيب. إذا كانت لديك أيّ تأثيرات جانبية تعتمد على واجهة المستخدم التي يتم عرضها، سيتم تطبيق التأثير الجانبي حتى في حال إلغاء عملية الإنشاء. وقد يؤدي ذلك إلى عدم اتساق حالة التطبيق.
تأكَّد من أنّ جميع الدوالّ القابلة للتجميع ودوالّ Lambda لا تؤدي إلى تكرار الإجراء ولا تتسبّب في أي آثار جانبية لمعالجة إعادة التركيب التفاؤلي.
قد يتم تشغيل الدوالّ القابلة للتجميع بشكلٍ متكرّر.
في بعض الحالات، قد يتم تشغيل دالة قابلة للتجميع لكل إطار من رسوم متحركة لواجهة المستخدم. إذا كانت الدالة تُجري عمليات مُكلّفة، مثل القراءة من ملف تخزين الجهاز، يمكن أن تتسبّب الدالة في حدوث تقطُّع في واجهة المستخدم.
على سبيل المثال، إذا حاول التطبيق المصغّر قراءة إعدادات الجهاز، قد يؤدي ذلك إلى قراءة تلك الإعدادات مئات المرات في الثانية، ما قد يؤثر بشكلٍ سلبي في أداء التطبيق.
إذا كانت الدالة القابلة للتجميع تحتاج إلى بيانات، يجب أن تحدِّد مَعلمات لتلك
البيانات. يمكنك بعد ذلك نقل العمل المكثّف إلى سلسلة محادثات أخرى خارج عملية الcomposing، ونقل البيانات إلى Compose باستخدام 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. ومن خلال حظر عمليات الكتابة هذه، نسمح للإطار الأساسي بتغيير المواضيع
لتنفيذ الدوالّ lambda القابلة للتركيب.
يمكن تنفيذ الدوالّ القابلة للتجميع بأي ترتيب.
إذا اطّلعت على رمز دالة قابلة للتجميع، قد تفترض أنّه يتم تنفيذ الرمز بالترتيب الذي يظهر به. ولا يمكن ضمان صحة هذه المعلومات. إذا كانت دالة قابلة للتجميع تحتوي على طلبات استدعاء لدوالّ قابلة للتجميع أخرى، قد يتم تنفيذ تلك الدوال بأي ترتيب. تتيح ميزة "الإنشاء" إمكانية التعرّف على أنّه للبعض عناصر واجهة المستخدم أولوية أعلى من غيرها، وبالتالي رسمها أولاً.
على سبيل المثال، لنفترض أنّ لديك رمزًا برمجيًا مثل هذا لرسم ثلاث شاشات في تنسيق علامة تبويب:
@Composable fun ButtonRow() { MyFancyNavigation { StartScreen() MiddleScreen() EndScreen() } }
قد يتم إجراء المكالمات إلى StartScreen
وMiddleScreen
وEndScreen
بأي ترتيب. وهذا يعني أنّه لا يمكنك، على سبيل المثال، ضبط بعض المتغيّرات العميقة (تأثير جانبي) في StartScreen()
والاستفادة من هذا التغيُّر في MiddleScreen()
. بدلاً من ذلك، يجب أن تكون كلّ من هذه الدوالّ مكتفية ذاتيًا.
مزيد من المعلومات
لمعرفة المزيد حول كيفية التفكير في Compose والدوالّ القابلة للتجميع، اطّلِع على الموارد الإضافية التالية.
الفيديوهات
أفلام مُقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما تكون لغة JavaScript غير مفعّلة.
- Kotlin لـ Jetpack Compose
- State وJetpack Compose
- البنية المُركّبة في Jetpack Compose