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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

دراسة الحالة: لتلبية متطلبات تطبيق "أخبار Google"، يمكن وضع المعلومات المطلوبة للعرض الكامل لواجهة المستخدم في فئة بيانات 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,
    ...
)

عدم التغيّر

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

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

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

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

function + UiState.

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

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

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

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

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

أصحاب الولاية

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

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

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

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

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

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

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

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

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

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

تلقي الأقسام التالية نظرة عن كثب على الأحداث التي تؤدي إلى حدوث تغييرات في الحالة وكيفية معالجتها باستخدام الأداة المعرَّفة من قِبل المستخدم (UDF).

أنواع المنطق

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

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

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

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

لماذا يُنصح باستخدام UDF؟

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

بمعنى آخر، يسمح النطاق المعزَّز (UDF) بما يلي:

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

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

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

المشاهدات

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

    val uiState: StateFlow<NewsUiState> = …
}

إنشاء

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

    val uiState: NewsUiState = …
}

للاطّلاع على مقدّمة عن LiveData كصاحب بيانات يمكن قياسها، راجِع هذا الدرس التطبيقي حول الترميز. للاطّلاع على مقدمة مشابهة عن تدفقات Kotlin، يمكنك الاطّلاع على مسارات Kotlin على Android.

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

إحدى الطرق الشائعة لإنشاء بث UiState هي استخدام بث مباشر خلفي قابل للتغيير كبث غير قابل للتغيير من ViewModel، على سبيل المثال، عرض MutableStateFlow<UiState> كـ StateFlow<UiState>.

المشاهدات

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    ...

}

إنشاء

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

    var uiState by mutableStateOf(NewsUiState())
        private set

    ...
}

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

المشاهدات

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

    private val _uiState = MutableStateFlow(NewsUiState())
    val uiState: StateFlow<NewsUiState> = _uiState.asStateFlow()

    private var fetchJob: Job? = null

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

إنشاء

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() على LiveData قد يكون ضروريًا.

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

للاطّلاع على تدفق عناصر UiState في واجهة المستخدم، يجب استخدام عامل التشغيل الطرفي لنوع البيانات القابلة للتتبّع الذي تستخدمه. على سبيل المثال، بالنسبة إلى LiveData، تستخدم الطريقة observe()، وبالنسبة إلى تدفقات Kotlin، تستخدم الطريقة collect() أو صيغها.

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

المشاهدات

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.uiState.collect {
                    // Update UI elements
                }
            }
        }
    }
}

إنشاء

@Composable
fun LatestNewsScreen(
    viewModel: NewsViewModel = viewModel()
) {
    // Show UI elements based on the viewModel.uiState
}

عرض العمليات الجارية

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

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

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

المشاهدات

class NewsActivity : AppCompatActivity() {

    private val viewModel: NewsViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        ...

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Bind the visibility of the progressBar to the state
                // of isFetchingArticles.
                viewModel.uiState
                    .map { it.isFetchingArticles }
                    .distinctUntilChanged()
                    .collect { progressBar.isVisible = it }
            }
        }
    }
}

إنشاء

@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(),
    ...
)

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

سلسلة التعليمات والتزامن

يجب أن يكون أي عمل يتم إجراؤه في ViewModel آمنًا، ويمكن الاتصال به من سلسلة التعليمات الرئيسية بشكل آمن. وذلك لأن طبقات البيانات والنطاق مسؤولة عن نقل العمل إلى مؤشر ترابط مختلف.

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

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

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

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

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

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

عيّنات

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