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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

لمزيد من المعلومات حول حالة واجهة المستخدم، راجِع الحالة وJetpack Compose.

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

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

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

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

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

functionality + UiState

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

أنواع المنطق

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

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

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

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

لماذا يجب استخدام الدوال المعرَّفة من قِبل المستخدم؟

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

بعبارة أخرى، تتيح الدوال المعرَّفة من قِبل المستخدم ما يلي:

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

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

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

عند استخدام دالة معرَّفة من قِبل المستخدم لإدارة إنتاج الحالة، يمكنك اعتبار الحالة المنتَجة بمثابة بث، أي يتم إنتاج إصدارات متعددة من الحالة بمرور الوقت. عرض حالة واجهة المستخدم في حاوية بيانات قابلة للمراقبة مثل 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().

لمزيد من المعلومات حول العرض وحالة واجهة المستخدم، راجِع مراحل نشاط العناصر القابلة للإنشاء.

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

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

عند استخدام عناصر تتضمّن بيانات قابلة للمراقبة في واجهة المستخدم، احرص على مراعاة مراحل نشاط واجهة المستخدم. لا تجعل واجهة المستخدم تراقب حالة واجهة المستخدم عندما لا يتم عرض العنصر القابل للإنشاء للمستخدم. لمزيد من المعلومات حول هذا الموضوع، يُرجى الاطّلاع على منشور المدوّنة هذا. عند استخدام التدفقات، من الأفضل معالجة المشاكل المتعلّقة بدورة الحياة باستخدام نطاق الروتين المشترك المناسب وواجهة برمجة التطبيقات 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 المتزامنة طريقة رائعة لإدارة العمليات المتزامنة، وتوفّر &quot;مكوّنات بنية Jetpack&quot; إمكانية استخدامها بشكل مدمج. لمزيد من المعلومات حول استخدام الكوروتينات في تطبيقات Android، يمكنك الاطّلاع على الكوروتينات في Kotlin على Android.

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

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

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

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

يوضّح المثال التالي واجهة برمجة التطبيقات Compose الخاصة بمكتبة 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 التالية كيفية استخدام طبقة واجهة المستخدم. يمكنك استكشافها للاطّلاع على هذه الإرشادات عمليًا: