طبقة البيانات

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

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

بنية طبقة البيانات

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

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

تكون فصول المستودعات مسؤولة عن المهام التالية:

  • عرض البيانات لبقية التطبيق.
  • مركزية التغييرات على البيانات.
  • حل التعارضات بين مصادر بيانات متعددة.
  • استخلاص مصادر البيانات من بقية التطبيق.
  • يتضمن منطق العمل.

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

يجب ألا تصل الطبقات الأخرى في التسلسل الهرمي إلى مصادر البيانات مباشرةً مطلقًا؛ وكانت نقاط الدخول إلى طبقة البيانات هي دائمًا فئات المستودع. يجب ألا تحتوي فئات البيانات التابعة للولاية (راجِع دليل طبقة واجهة المستخدم) أو فئات حالات الاستخدام (راجِع دليل طبقة النطاق) على مصدر بيانات كتبعية مباشرة. يسمح استخدام فئات المستودع كنقاط دخول للطبقات المختلفة للبنية الهندسية بالتوسّع بشكل مستقل.

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

باتّباع أفضل ممارسات إدخال التبعية، يأخذ المستودع مصادر البيانات كتبعيات في الدالة الإنشائية له:

class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) { /* ... */ }

عرض واجهات برمجة التطبيقات

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

  • العمليات لمرة واحدة: ينبغي أن تعرض طبقة البيانات دوال التعليق في Kootlin، وبالنسبة إلى لغة البرمجة Java، يُفترض أن تكشف طبقة البيانات عن دوال توفّر استدعاء لإبلاغ نتيجة العملية، أو أنواع RxJava Single أو Maybe أو Completable.
  • ليتم إعلامك بالتغييرات التي تطرأ على البيانات بمرور الوقت: يجب أن تعرض طبقة البيانات التدفقات في Kotlin. وبالنسبة إلى لغة البرمجة Java، يجب أن تُظهر طبقة البيانات رد اتصال يصدر البيانات الجديدة، أو نوع RxJava Observable أو Flowable.
class ExampleRepository(
    private val exampleRemoteDataSource: ExampleRemoteDataSource, // network
    private val exampleLocalDataSource: ExampleLocalDataSource // database
) {

    val data: Flow<Example> = ...

    suspend fun modifyData(example: Example) { ... }
}

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

في هذا الدليل، تتم تسمية فئات المستودع باسم البيانات التي تكون مسؤولة عنها. والاصطلاح هو كما يلي:

type of data + مستودع.

على سبيل المثال: NewsRepository أو MoviesRepository أو PaymentsRepository

تتم تسمية فئات مصدر البيانات بعد البيانات التي تكون مسئولة عنها والمصدر الذي تستخدمها. والاصطلاح هو كما يلي:

نوع البيانات + نوع المصدر + مصدر البيانات.

بالنسبة إلى نوع البيانات، استخدِم عن بُعد أو محلي ليكون أكثر عمومية لأن عمليات التنفيذ يمكن أن تتغيّر. على سبيل المثال: NewsRemoteDataSource أو NewsLocalDataSource لكي تكون أكثر تحديدًا في حالة أهمية المصدر، استخدم نوع المصدر. على سبيل المثال: NewsNetworkDataSource أو NewsDiskDataSource

لا تُسمّى مصدر البيانات استنادًا إلى تفاصيل التنفيذ، مثلاً UserSharedPreferencesDataSource، لأنّ المستودعات التي تستخدم مصدر البيانات هذا يجب ألا تعرف طريقة حفظ البيانات. إذا اتّبعت هذه القاعدة، يمكنك تغيير تنفيذ مصدر البيانات (على سبيل المثال، نقل البيانات من SharedPreferences إلى DataStore) بدون التأثير في الطبقة التي تستدعي هذا المصدر.

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

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

على سبيل المثال، قد يعتمد المستودع الذي يعالج بيانات مصادقة المستخدم UserRepository على مستودعات أخرى، مثل LoginRepository وRegistrationRepository، لتلبية متطلباته.

في هذا المثال، يعتمد UserRepository على فئتين أخريين من فئات المستودع:
    LoginRepository, الذي يعتمد على مصادر أخرى لبيانات تسجيل الدخول،
    وRegistrationRepository التي تعتمد على مصادر بيانات التسجيل الأخرى.
الشكل 2. رسم بياني للتبعية لمستودع يعتمد على مستودعات أخرى.

مصدر الحقيقة

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

قد يكون مصدر الحقيقة مصدر بيانات - على سبيل المثال قاعدة البيانات - أو حتى ذاكرة تخزين مؤقت في الذاكرة قد يحتوي عليها المستودع. تجمع المستودعات مصادر بيانات مختلفة وتحل أي تضاربات محتملة بين مصادر البيانات لتحديث مصدر الحقيقة الوحيد بانتظام أو بسبب حدث أدخله المستخدم.

قد تحتوي المستودعات المختلفة في تطبيقك على مصادر مختلفة للحقيقة. على سبيل المثال، قد تستخدم الفئة LoginRepository ذاكرة التخزين المؤقت الخاصة بها كمصدر للحقيقة، وقد تستخدم الفئة PaymentsRepository مصدر بيانات الشبكة.

لتوفير دعم في وضع عدم الاتصال بالإنترنت أولاً، يكون مصدر البيانات المحلي، مثل قاعدة بيانات، هو مصدر الحقيقة الذي يُنصَح به.

سلسلة المحادثات

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

تجدر الإشارة إلى أنّ معظم مصادر البيانات توفّر واجهات برمجة تطبيقات آمنة بشكل رئيسي، مثل استدعاءات طريقة التعليق التي توفّرها Room أو Retrofit أو Ktor. ويمكن لمستودعك الاستفادة من واجهات برمجة التطبيقات هذه عند توفّرها.

لمزيد من المعلومات حول سلاسل المحادثات، راجِع دليل المعالجة في الخلفية. بالنسبة إلى مستخدمي لغة Kotlin، تُعدّ الكوروتينات الخيار المقترَح. راجِع تشغيل مهام Android في سلاسل المحادثات في الخلفية للاطّلاع على الخيارات المقترَحة للغة برمجة Java.

مراحل النشاط

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

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

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

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

تمثيل نماذج الأنشطة التجارية

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

على سبيل المثال، تخيَّل أنّ خادمًا لواجهة برمجة تطبيقات "أخبار Google" لا يعرض معلومات المقالة فحسب، بل يعرض أيضًا سجلّ التعديلات وتعليقات المستخدمين وبعض البيانات الوصفية:

data class ArticleApiModel(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val modifications: Array<ArticleApiModel>,
    val comments: Array<CommentApiModel>,
    val lastModificationDate: Date,
    val authorId: Long,
    val authorName: String,
    val authorDateOfBirth: Date,
    val readTimeMin: Int
)

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

data class Article(
    val id: Long,
    val title: String,
    val content: String,
    val publicationDate: Date,
    val authorName: String,
    val readTimeMin: Int
)

يُعد فصل فئات النموذج مفيدًا من الناحية التالية:

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

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

أنواع عمليات البيانات

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

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

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

يتم عادةً تشغيل العمليات التي تتعلق بواجهة المستخدم بواسطة طبقة واجهة المستخدم وتتبع دورة حياة المتصل - على سبيل المثال، دورة حياة ViewModel. راجع قسم تقديم طلب شبكة للحصول على مثال على عملية موجّهة إلى واجهة المستخدم.

العمليات المستنِدة إلى التطبيقات

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

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

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

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

توصية العمليات ذات النهج التجاري هي استخدام WorkManager. راجع قسم جدولة المهام باستخدام WorkManager لمزيد من المعلومات.

الكشف عن الأخطاء

يمكن أن تنجح التفاعلات مع مستودعات ومصادر البيانات أو تؤدي إلى ظهور استثناء عند حدوث الفشل. بالنسبة إلى الكوروتينات والتدفقات، يجب استخدام آلية معالجة الأخطاء المدمَجة في Kotlin. بالنسبة إلى الأخطاء التي يمكن أن تظهر من خلال تعليق الدوال، استخدِم عمليات حظر try/catch عندما يكون ذلك مناسبًا. وفي التدفقات، استخدِم عامل التشغيل catch. من خلال هذا النهج، من المتوقع أن تتعامل طبقة واجهة المستخدم مع الاستثناءات عند استدعاء طبقة البيانات.

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

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

مهام شائعة

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

تقديم طلب شبكة

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

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

إنشاء مصدر البيانات

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

إنّ تقديم طلب الشبكة هو عبارة عن مكالمة لمرة واحدة تتم معالجتها باستخدام طريقة fetchLatestNews() جديدة:

class NewsRemoteDataSource(
  private val newsApi: NewsApi,
  private val ioDispatcher: CoroutineDispatcher
) {
    /**
     * Fetches the latest news from the network and returns the result.
     * This executes on an IO-optimized thread pool, the function is main-safe.
     */
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        // Move the execution to an IO-optimized thread since the ApiService
        // doesn't support coroutines and makes synchronous requests.
        withContext(ioDispatcher) {
            newsApi.fetchLatestNews()
        }
    }

// Makes news-related network synchronous requests.
interface NewsApi {
    fun fetchLatestNews(): List<ArticleHeadline>
}

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

إنشاء المستودع

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

// NewsRepository is consumed from other layers of the hierarchy.
class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource
) {
    suspend fun fetchLatestNews(): List<ArticleHeadline> =
        newsRemoteDataSource.fetchLatestNews()
}

لمعرفة كيفية استهلاك فئة المستودع مباشرةً من طبقة واجهة المستخدم، راجِع دليل طبقة واجهة المستخدم.

تنفيذ ميزة التخزين المؤقت للبيانات في الذاكرة

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

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

وحدات ذاكرة التخزين المؤقت

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

تخزين نتيجة طلب الشبكة في ذاكرة التخزين المؤقت

لتبسيط الأمر، يستخدم NewsRepository متغيّرًا قابلاً للتغيير لحفظ آخر الأخبار مؤقتًا. لحماية عمليات القراءة والكتابة من سلاسل المحادثات المختلفة، يتم استخدام Mutex. للاطّلاع على مزيد من المعلومات عن حالة التغيير المشترك والتزامن، راجِع مستندات Kotlin.

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

class NewsRepository(
  private val newsRemoteDataSource: NewsRemoteDataSource
) {
    // Mutex to make writes to cached values thread-safe.
    private val latestNewsMutex = Mutex()

    // Cache of the latest news got from the network.
    private var latestNews: List<ArticleHeadline> = emptyList()

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        if (refresh || latestNews.isEmpty()) {
            val networkResult = newsRemoteDataSource.fetchLatestNews()
            // Thread-safe write to latestNews
            latestNewsMutex.withLock {
                this.latestNews = networkResult
            }
        }

        return latestNewsMutex.withLock { this.latestNews }
    }
}

جعل العملية لفترة أطول من الشاشة

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

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

class NewsRepository(
    ...,
    // This could be CoroutineScope(SupervisorJob() + Dispatchers.Default).
    private val externalScope: CoroutineScope
) { ... }

بما أنّ NewsRepository جاهز لتنفيذ عمليات موجَّهة إلى التطبيق باستخدام CoroutineScope الخارجي، يجب تنفيذ طلب بيانات لمصدر البيانات وحفظ نتيجته باستخدام الكوروتين الجديد الذي بدأه هذا النطاق:

class NewsRepository(
    private val newsRemoteDataSource: NewsRemoteDataSource,
    private val externalScope: CoroutineScope
) {
    /* ... */

    suspend fun getLatestNews(refresh: Boolean = false): List<ArticleHeadline> {
        return if (refresh) {
            externalScope.async {
                newsRemoteDataSource.fetchLatestNews().also { networkResult ->
                    // Thread-safe write to latestNews.
                    latestNewsMutex.withLock {
                        latestNews = networkResult
                    }
                }
            }.await()
        } else {
            return latestNewsMutex.withLock { this.latestNews }
        } 
    }
}

يتم استخدام async لبدء الكوروتين في النطاق الخارجي. يتم استدعاء await في الكوروتين الجديد للتعليق حتى يأتي طلب الشبكة ويتم حفظ النتيجة في ذاكرة التخزين المؤقت. إذا كان المستخدم لا يزال على الشاشة بحلول ذلك الوقت، سيرى آخر الأخبار، وإذا ابتعد المستخدم عن الشاشة، سيتم إلغاء علامة await مع استمرار تنفيذ المنطق داخل async.

يمكنك الاطّلاع على مشاركة المدوّنة هذه للمزيد من المعلومات حول أنماط CoroutineScope.

حفظ البيانات واستردادها من القرص

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

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

  • بالنسبة إلى مجموعات البيانات الكبيرة التي يجب الاستعلام عنها أو تحتاج إلى تكامل مرجعي أو تحتاج إلى تعديلات جزئية، احفظ البيانات في قاعدة بيانات الغرفة. وفي مثال تطبيق الأخبار، يمكن حفظ المقالات الإخبارية أو المؤلفين في قاعدة البيانات
  • بالنسبة إلى مجموعات البيانات الصغيرة التي يجب استردادها وضبطها فقط (وليس طلبات البحث أو تعديلها جزئيًا)، يمكنك استخدام DataStore. في مثال تطبيق "أخبار Google"، يمكن حفظ تنسيق التاريخ المفضّل لدى المستخدم أو إعدادات العرض المفضّلة الأخرى في DataStore.
  • بالنسبة إلى مجموعات البيانات، مثل كائن JSON، استخدِم file.

كما هو مذكور في قسم مصدر البيانات، يعمل كل مصدر بيانات مع مصدر واحد فقط ويتوافق مع نوع معيّن من البيانات (على سبيل المثال، News أو Authors أو NewsAndAuthors أو UserPreferences). ويجب ألا تعرف الفئات التي تستخدم مصدر البيانات طريقة حفظ البيانات، مثلاً في قاعدة بيانات أو في ملف.

الغرفة كمصدر للبيانات

بما أنّه يجب أن يتحمل كل مصدر بيانات مسؤولية العمل مع مصدر واحد فقط لنوع معيّن من البيانات، سيتلقّى مصدر بيانات الغرفة كائن الوصول إلى البيانات (DAO) أو قاعدة البيانات نفسها كمَعلمة. على سبيل المثال، قد تأخذ NewsLocalDataSource مثيل NewsDao كمَعلمة، وAuthorsLocalDataSource قد تأخذ مثيلاً لـ AuthorsDao.

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

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

DataStore كمصدر بيانات

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

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

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

لمزيد من المعلومات عن استخدام واجهات برمجة تطبيقات DataStore API، يُرجى الاطّلاع على أدلة DataStore.

ملف كمصدر بيانات

عند التعامل مع كائنات كبيرة مثل كائن JSON أو صورة نقطية، يجب استخدام كائن File والتعامل مع تبديل سلاسل التعليمات.

لمعرفة المزيد من المعلومات حول استخدام مساحة تخزين الملفات، يمكنك الاطّلاع على صفحة نظرة عامة على مساحة التخزين.

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

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

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

class RefreshLatestNewsWorker(
    private val newsRepository: NewsRepository,
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result = try {
        newsRepository.refreshLatestNews()
        Result.success()
    } catch (error: Throwable) {
        Result.failure()
    }
}

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

في هذا المثال، يجب استدعاء هذه المهمة المتعلقة بالأخبار من NewsRepository، والتي قد تأخذ مصدر بيانات جديدًا كتبعية: NewsTasksDataSource، وسيتم تنفيذها على النحو التالي:

private const val REFRESH_RATE_HOURS = 4L
private const val FETCH_LATEST_NEWS_TASK = "FetchLatestNewsTask"
private const val TAG_FETCH_LATEST_NEWS = "FetchLatestNewsTaskTag"

class NewsTasksDataSource(
    private val workManager: WorkManager
) {
    fun fetchNewsPeriodically() {
        val fetchNewsRequest = PeriodicWorkRequestBuilder<RefreshLatestNewsWorker>(
            REFRESH_RATE_HOURS, TimeUnit.HOURS
        ).setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.TEMPORARILY_UNMETERED)
                .setRequiresCharging(true)
                .build()
        )
            .addTag(TAG_FETCH_LATEST_NEWS)

        workManager.enqueueUniquePeriodicWork(
            FETCH_LATEST_NEWS_TASK,
            ExistingPeriodicWorkPolicy.KEEP,
            fetchNewsRequest.build()
        )
    }

    fun cancelFetchingNewsPeriodically() {
        workManager.cancelAllWorkByTag(TAG_FETCH_LATEST_NEWS)
    }
}

تتم تسمية هذه الأنواع من الفئات بعد البيانات المسؤولة عنها، على سبيل المثال، NewsTasksDataSource أو PaymentsTasksDataSource. يجب حزم جميع المهام المتعلقة بنوع معين من البيانات في نفس الفئة.

وإذا كان هناك حاجة إلى تشغيل المهمة عند بدء تشغيل التطبيق، نقترح تشغيل طلب WorkManager باستخدام مكتبة App Startup التي تستدعي المستودع من Initializer.

لمزيد من المعلومات عن استخدام واجهات برمجة تطبيقات WorkManager، يُرجى الاطّلاع على أدلة WorkManager.

الاختبار

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

اختبارات الوحدات

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

اختبارات الدمج

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

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

أما بالنسبة إلى الربط الشبكي، فهناك مكتبات شائعة مثل WireMock أو MockWebServer، والتي تتيح لك تزييف مكالمات HTTP وHTTPS، والتحقّق من أنّه تم تقديم الطلبات على النحو المتوقَّع.

عيّنات

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