طبقة واجهة المستخدم

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

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

في البنية النموذجية، تعتمد عناصر واجهة المستخدم في طبقة واجهة المستخدم على عناصر الاحتفاظ بالحالة، والتي تعتمد بدورها على فئات من طبقة البيانات أو طبقة النطاق الاختيارية.
الشكل 1: دور طبقة واجهة المستخدم في بنية التطبيق

دراسة حالة أساسية

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

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

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

بنية طبقة واجهة المستخدم

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

  1. استخدام بيانات التطبيق وتحويلها إلى بيانات يمكن لواجهة المستخدم عرضها بسهولة
  2. استخدام البيانات القابلة للعرض في واجهة المستخدم وتحويلها إلى عناصر في واجهة المستخدم لعرضها للمستخدم.
  3. استخدِم أحداث إدخال المستخدم من عناصر واجهة المستخدم المجمّعة هذه واعرض تأثيراتها في بيانات واجهة المستخدم حسب الحاجة.
  4. كرِّر الخطوات من 1 إلى 3 حسب الحاجة.

يوضّح الجزء المتبقّي من هذا الدليل كيفية تنفيذ طبقة واجهة مستخدم تنفّذ هذه الخطوات. على وجه التحديد، يتناول هذا الدليل المهام والمفاهيم التالية:

  • كيفية تحديد حالة واجهة المستخدم
  • تدفق البيانات أحادي الاتجاه (UDF) كوسيلة لإنتاج حالة واجهة المستخدم وإدارتها
  • كيفية عرض حالة واجهة المستخدم باستخدام أنواع البيانات القابلة للتتبّع وفقًا لمبادئ تدفق البيانات أحادي الاتجاه
  • كيفية تنفيذ واجهة مستخدم تستخدم حالة واجهة المستخدم القابلة للتتبّع

وأهم هذه العناصر هو تعريف حالة واجهة المستخدم.

تحديد حالة واجهة المستخدم

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

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

تنتج واجهة المستخدم عن ربط عناصر في واجهة المستخدم على الشاشة بحالة واجهة المستخدم.
الشكل 3: واجهة المستخدم هي نتيجة ربط عناصر واجهة المستخدم على الشاشة بحالة واجهة المستخدم.

لنفترض أنّنا نريد تلبية متطلبات تطبيق الأخبار، يمكننا تضمين المعلومات المطلوبة لعرض واجهة المستخدم بالكامل في فئة بيانات NewsUiState على النحو التالي:

data class NewsUiState(
    val isSignedIn: Boolean = false,
    val isPremium: Boolean = false,
    val newsItems: List<NewsItemUiState> = listOf(),
    val userMessages: List<Message> = listOf()
)

data class NewsItemUiState(
    val title: String,
    val body: String,
    val bookmarked: Boolean = false,
    // ...
)

لمزيد من المعلومات حول حالة واجهة المستخدم، يُرجى الاطّلاع على State وJetpack Compose.

عدم قابلية التغيير

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

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

اصطلاحات التسمية في هذا الدليل

في هذا الدليل، تتم تسمية فئات حالة واجهة المستخدم استنادًا إلى وظيفة الشاشة أو جزء الشاشة الذي تصفه. في ما يلي الاصطلاح:

functionality + UiState.

على سبيل المثال، قد تُسمى حالة الشاشة التي تعرض الأخبار NewsUiState، وقد تكون حالة خبر في قائمة الأخبار NewsItemUiState.

إدارة الحالة باستخدام تدفق البيانات أحادي الاتجاه

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

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

يتناول هذا القسم تدفق البيانات أحادي الاتجاه (UDF)، وهو نمط بنية يساعد في فرض هذا الفصل السليم للمسؤولية.

عناصر الاحتفاظ بالحالة

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

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

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

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

يُطلق على النمط الذي تتدفق فيه الحالة لأسفل وتتدفق الأحداث لأعلى اسم تدفق البيانات أحادي الاتجاه (UDF). في ما يلي الآثار المترتبة على هذا النمط في بنية التطبيق:

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

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

عنصر مقالة واحد من تطبيق دراسة الحالة. تعرض واجهة المستخدم صورة مصغّرة وعنوان المقالة والمؤلف والوقت المقدَّر لقراءة المقالة ورمز إشارة مرجعية.
الشكل 5: واجهة مستخدم لمقالة في تطبيق دراسة الحالة

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

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

تلقي الأقسام التالية نظرة فاحصة على الأحداث التي تسبب تغييرات في الحالة وكيف يمكن معالجتها باستخدام UDF.

أنواع المنطق

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

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

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

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

لماذا يجب استخدام UDF؟

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

بعبارة أخرى، تتيح ميزة UDF ما يلي:

  • اتساق البيانات: هناك مصدر وحيد للحقيقة لواجهة المستخدم.
  • قابلية الاختبار: يتم عزل مصدر الحالة وبالتالي يمكن اختباره بشكل مستقل عن واجهة المستخدم.
  • قابلية الصيانة: يتبع تغيير الحالة نمطًا محددًا جيدًا حيث تكون التغييرات نتيجة لكل من أحداث المستخدم ومصادر البيانات التي يتم سحبها منها.

عرض حالة واجهة المستخدم

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

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

class NewsViewModel(
    // ...
) : ViewModel() {

    val uiState: NewsUiState = /* ... */
}

للحصول على مقدّمة عن تدفقات Kotlin، يمكنك الاطّلاع على تدفقات Kotlin على Android. لمعرفة كيفية استخدام StateFlow كحاوية بيانات قابلة للتتبّع، يُرجى الاطّلاع على الدرس التطبيقي حول الترميز الحالة المتقدّمة والآثار الجانبية في Jetpack Compose.

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

من الطرق الشائعة لإنشاء سلسلة من UiState هي عرض سمة mutableStateOf باستخدام private set، مع إبقاء الحالة قابلة للتغيير داخل ViewModel ولكن للقراءة فقط لواجهة المستخدم.

class NewsViewModel(
    // ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    // ...
}

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

class NewsViewModel(
    private val repository: NewsRepository,
    // ...
) : ViewModel() {

    var uiState by mutableStateOf(NewsUiState())
        private set

    private var fetchJob: Job? = null

    fun fetchArticles(category: String) {
        fetchJob?.cancel()
        fetchJob = viewModelScope.launch {
            try {
                val newsItems = repository.newsItemsForCategory(category)
                uiState = uiState.copy(newsItems = newsItems)
            } catch (ioe: IOException) {
                // Handle the error and notify the UI when appropriate.
                val messages = getMessagesFromThrowable(ioe)
                uiState = uiState.copy(userMessages = messages)
            }
        }
    }
}

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

اعتبارات أخرى

بالإضافة إلى الإرشادات السابقة، يجب مراعاة ما يلي عند عرض حالة واجهة المستخدم:

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

    data class NewsUiState(
        val isSignedIn: Boolean = false,
        val isPremium: Boolean = false,
        val newsItems: List<NewsItemUiState> = listOf()
    )
    
    val NewsUiState.canBookmarkNews: Boolean get() = isSignedIn && isPremium

    في هذا التعريف، يكون ظهور زر الإشارة المرجعية خاصية مشتقة من خاصيتين أخريين. مع زيادة تعقيد منطق النشاط التجاري، تزداد أهمية توفُّر فئة UiState واحدة تكون فيها جميع السمات متاحة على الفور.

  • حالات واجهة المستخدم: بث واحد أو عدة أحداث بث؟ المبدأ التوجيهي الرئيسي للاختيار بين عرض حالة واجهة المستخدم في بث واحد أو في بث متعدد هو العلاقة بين العناصر المنبعثة. من أهم مزايا عرض البيانات في مسار واحد هي السهولة واتساق البيانات، حيث يحصل مستهلكو الحالة دائمًا على أحدث المعلومات المتاحة في أي وقت. ومع ذلك، هناك حالات قد يكون فيها استخدام تدفقات منفصلة للحالة من ViewModel مناسبًا:

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

    • مقارنة UiState: كلما زاد عدد الحقول في عنصر UiState، زاد احتمال أن يصدر البث نتيجة تعديل أحد حقوله. بما أنّ عناصر واجهة المستخدم لا تتضمّن آلية مقارنة لمعرفة ما إذا كانت عمليات الإرسال المتتالية مختلفة أو متطابقة، فإنّ كل عملية إرسال تؤدي إلى تعديل عنصر في واجهة المستخدم. هذا يعني أنّه قد يكون من الضروري استخدام طرق واجهة برمجة التطبيقات Flow، مثل distinctUntilChanged()، لتخفيف الأثر.

لمزيد من المعلومات حول العرض وحالة واجهة المستخدم، يُرجى الاطّلاع على مراحل نشاط الدوال المركّبة.

استخدام حالة واجهة المستخدم

To consume the stream of UiState objects in the UI, use the terminal operator for the observable data type that you're using. على سبيل المثال، بالنسبة إلى تدفقات Kotlin، استخدِم طريقة collect() أو صيغها المختلفة.

When consuming observable data holders in the UI, make sure to consider the lifecycle of the UI. لا تجعل واجهة المستخدم تراقب حالة واجهة المستخدم عندما لا يتم عرض الدالة المركّبة للمستخدم. لمزيد من المعلومات حول هذا الموضوع، يُرجى الاطّلاع على منشور المدوّنة هذا. عند استخدام التدفقات، من الأفضل التعامل مع المشاكل المتعلّقة بمراحل النشاط من خلال نطاق الكوروتين المناسب وواجهة برمجة التطبيقات collectAsStateWithLifecycle:

@Composable
private fun ConversationScreen(
    conversationViewModel: ConversationViewModel = viewModel()
) {

    val messages by conversationViewModel.messages.collectAsStateWithLifecycle()

    ConversationScreen(
        messages = messages,
        onSendMessage = { message: Message -> conversationViewModel.sendMessage(message) }
    )
}

@Composable
private fun ConversationScreen(
    messages: List<Message>,
    onSendMessage: (Message) -> Unit
) {

    MessagesList(messages, onSendMessage)
    /* ... */
}

عرض العمليات قيد التقدّم

يمكن تمثيل حالات التحميل في فئة UiState بطريقة بسيطة باستخدام حقل منطقي:

data class NewsUiState(
    val isFetchingArticles: Boolean = false,
    // ...
)

تمثّل قيمة هذه العلامة توفُّر شريط تقدّم في واجهة المستخدم أو عدم توفّره.

@Composable
fun LatestNewsScreen(
    modifier: Modifier = Modifier,
    viewModel: NewsViewModel = viewModel()
) {
    Box(modifier.fillMaxSize()) {

        if (viewModel.uiState.isFetchingArticles) {
            CircularProgressIndicator(Modifier.align(Alignment.Center))
        }

        // Add other UI elements. For example, the list.
    }
}

عرض الأخطاء على الشاشة

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

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

data class Message(val id: Long, val message: String)

data class NewsUiState(
    val userMessages: List<Message> = listOf(),
    // ...
)

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

المعالجة المتزامنة والتزامن

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

إذا كان ViewModel ينفّذ عمليات طويلة، يكون مسؤولاً أيضًا عن نقل هذا المنطق إلى سلسلة مهام في الخلفية. تُعدّ وظائف Kotlin coroutines طريقة رائعة لإدارة العمليات المتزامنة، وتوفّر مكوّنات Jetpack Architecture Components دعمًا مضمّنًا لها. لمزيد من المعلومات حول استخدام الكوروتينات في تطبيقات Android، يُرجى الاطّلاع على الكوروتينات في Kotlin على Android.

غالبًا ما تكون التغييرات في التنقّل داخل التطبيق ناتجة عن أحداث معيّنة. على سبيل المثال، بعد أن تُجري فئة SignInViewModel عملية تسجيل دخول، قد يتضمّن UiState حقل isSignedIn تم ضبطه على true. يمكنك استخدام عوامل التشغيل هذه تمامًا مثل تلك التي تناولناها في قسم استخدام حالة واجهة المستخدم السابق، ولكن عليك تأجيل تنفيذ الاستخدام إلى مكوّن التنقّل.

لمزيد من المعلومات حول التنقّل في واجهة المستخدم، يُرجى الاطّلاع على التنقّل 3.

ترقيم الصفحات

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

يوضّح المثال التالي واجهة برمجة التطبيقات Compose API في مكتبة Paging:

@Composable
fun MyScreen(flow: Flow<PagingData<String>>) {
    val lazyPagingItems = flow.collectAsLazyPagingItems()
    LazyColumn {
        items(
            lazyPagingItems.itemCount,
            key = lazyPagingItems.itemKey { it }
        ) { index ->
            val item = lazyPagingItems[index]
            Text("Item is $item")
        }
    }
}

الصور المتحركة

لتوفير انتقالات سلسة للتنقل في المستوى الأعلى، قد ترغب في انتظار الشاشة الثانية لتحميل البيانات قبل بدء الرسوم المتحركة.

لمزيد من المعلومات عن انتقالات التنقّل، يُرجى الاطّلاع على Navigation 3 وانتقالات العناصر المشتركة في Compose.

مراجع إضافية

محتوى المشاهدات

نماذج

توضّح نماذج Google التالية كيفية استخدام طبقة واجهة المستخدم. يمكنك استكشافها للاطّلاع على هذه الإرشادات عمليًا: