إنشاء تطبيق بلا اتصال بالإنترنت أولاً

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

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

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

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

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

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

غالبًا ما يُطلق على التطبيق الذي يمكنه استيفاء المعايير أعلاه اسم "تطبيق يعمل بلا اتصال بالإنترنت أولاً".

تصميم تطبيق يعمل بلا اتصال بالإنترنت أولاً

عند تصميم تطبيق يعمل بلا اتصال بالإنترنت أولاً، يجب أن تبدأ بطبقة البيانات والعمليتَين الرئيسيتَين اللتَين يمكنك تنفيذهما على بيانات التطبيق:

  • القراءات: يتم استرداد البيانات لتستخدمها أجزاء أخرى من التطبيق مثل عرض المعلومات للمستخدم.
  • الكتب: الاحتفاظ بما يدخِله المستخدم لاسترداده لاحقًا.

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

وضع نماذج للبيانات في تطبيق بلا اتصال بالإنترنت أولاً

يحتوي التطبيق الذي يعمل بلا اتصال بالإنترنت أولاً على مصدرَي بيانات على الأقل لكل مستودع يستخدم موارد الشبكة:

  • مصدر البيانات المحلي
  • مصدر بيانات الشبكة
تتكون طبقة البيانات الأولى بلا اتصال بالإنترنت من مصادر البيانات المحلية ومصادر البيانات الخاصة بالشبكة.
الشكل 1: مستودع أول بلا اتصال بالإنترنت

مصدر البيانات المحلي

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

  • مصادر البيانات المنظَّمة، مثل قواعد البيانات الارتباطية، مثل Room
  • هي مصادر البيانات غير المهيكلة. على سبيل المثال، الموارد الاحتياطية للبروتوكولات مع مخزن البيانات.
  • ملفات بسيطة

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

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

عرض الموارد

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

تهدف بنية الدليل أدناه إلى تمثيل هذا المفهوم. تمثّل AuthorEntity تمثيلاً لمؤلف تم قراءته من قاعدة البيانات المحلية للتطبيق، وNetworkAuthor تمثيل لمؤلف متسلسل على الشبكة:

data/
├─ local/
│ ├─ entities/
│ │ ├─ AuthorEntity
│ ├─ dao/
│ ├─ NiADatabase
├─ network/
│ ├─ NiANetwork
│ ├─ models/
│ │ ├─ NetworkAuthor
├─ model/
│ ├─ Author
├─ repository/

في ما يلي تفاصيل AuthorEntity وNetworkAuthor:

/**
 * Network representation of [Author]
 */
@Serializable
data class NetworkAuthor(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

/**
 * Defines an author for either an [EpisodeEntity] or [NewsResourceEntity].
 * It has a many-to-many relationship with both entities
 */
@Entity(tableName = "authors")
data class AuthorEntity(
    @PrimaryKey
    val id: String,
    val name: String,
    @ColumnInfo(name = "image_url")
    val imageUrl: String,
    @ColumnInfo(defaultValue = "")
    val twitter: String,
    @ColumnInfo(name = "medium_page", defaultValue = "")
    val mediumPage: String,
    @ColumnInfo(defaultValue = "")
    val bio: String,
)

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

/**
 * External data layer representation of a "Now in Android" Author
 */
data class Author(
    val id: String,
    val name: String,
    val imageUrl: String,
    val twitter: String,
    val mediumPage: String,
    val bio: String,
)

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

/**
 * Converts the network model to the local model for persisting
 * by the local data source
 */
fun NetworkAuthor.asEntity() = AuthorEntity(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

/**
 * Converts the local model to the external model for use
 * by layers external to the data layer
 */
fun AuthorEntity.asExternalModel() = Author(
    id = id,
    name = name,
    imageUrl = imageUrl,
    twitter = twitter,
    mediumPage = mediumPage,
    bio = bio,
)

القراءة

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

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

class OfflineFirstTopicsRepository(
    private val topicDao: TopicDao,
    private val network: NiaNetworkDataSource,
) : TopicsRepository {

    override fun getTopicsStream(): Flow<List<Topic>> =
        topicDao.getTopicEntitiesStream()
            .map { it.map(TopicEntity::asExternalModel) }
}

حدوث أخطاء أثناء معالجة الاستراتيجيات

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

مصدر البيانات المحلية

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

يكون استخدام عامل التشغيل catch في ViewModel على النحو التالي:

class AuthorViewModel(
    authorsRepository: AuthorsRepository,
    ...
) : ViewModel() {
   private val authorId: String = ...

   // Observe author information
    private val authorStream: Flow<Author> =
        authorsRepository.getAuthorStream(
            id = authorId
        )
        .catch { emit(Author.empty()) }
}

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

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

تراجع أسي

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

قراءة البيانات باستخدام تراجع أسي
الشكل 2: قراءة البيانات باستخدام تراجع أسي

تتضمن معايير تقييم ما إذا كان يجب أن يستمر التطبيق في التراجع تتضمن ما يلي:

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

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

قراءة البيانات باستخدام أدوات مراقبة الشبكة وقوائم الانتظار
الشكل 3: قراءة قوائم الانتظار مع مراقبة الشبكة

يكتب

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

interface UserDataRepository {
    /**
     * Updates the bookmarked status for a news resource
     */
    suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}

في المقتطف أعلاه، واجهة برمجة التطبيقات غير المتزامنة التي تختارها هي Coroutines باعتبارها الطريقة أعلاه لإجراء عمليات التعليق.

كتابة الاستراتيجيات

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

عمليات الكتابة على الإنترنت فقط

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

إمكانية الكتابة على الإنترنت فقط
الشكل 4: يكتب على الإنترنت فقط

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

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

عمليات الكتابة في قائمة الانتظار

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

كتابة قوائم الانتظار باستخدام عمليات إعادة المحاولة
الشكل 5: كتابة قوائم الانتظار باستخدام إعادة المحاولة

يكون هذا المنهج خيارًا جيدًا في الحالات التالية:

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

تتضمن حالات الاستخدام لهذا المنهج أحداث الإحصاءات والتسجيل.

الكتابة الكسولة

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

الكتابة الكسولة من خلال مراقبة الشبكة
الشكل 6: عمليات الكتابة الكسولة

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

المزامنة وحل التعارضات

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

  • المزامنة المستندة إلى السحب
  • المزامنة المستندة إلى الدفع

المزامنة المستندة إلى السحب

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

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

المزامنة المستندة إلى السحب
الشكل 7: المزامنة المستندة إلى السحب: يصل الجهاز "أ" إلى الموارد للشاشتين "أ" و"ب" فقط، في حين يصل الجهاز "ب" إلى الموارد للشاشات "ب" و"ج" و"د" فقط

ضع في اعتبارك تطبيقًا يُستخدم فيه الرموز المميزة للصفحة لجلب عناصر في قائمة تمرير لا نهاية لها لشاشة معينة. قد يتواصل التنفيذ ببطء مع الشبكة، ويحتفظ بالبيانات في مصدر البيانات المحلي، ثم يقرأ من مصدر البيانات المحلي لعرض المعلومات للمستخدم. وفي حال عدم وجود اتصال بالشبكة، قد يطلب المستودع بيانات من مصدر البيانات المحلي وحده. هذا هو النمط الذي تستخدمه Jetpack Paging Library مع واجهة برمجة تطبيقات RemoteMediator.

class FeedRepository(...) {

    fun feedPagingSource(): PagingSource<FeedItem> { ... }
}

class FeedViewModel(
    private val repository: FeedRepository
) : ViewModel() {
    private val pager = Pager(
        config = PagingConfig(
            pageSize = NETWORK_PAGE_SIZE,
            enablePlaceholders = false
        ),
        remoteMediator = FeedRemoteMediator(...),
        pagingSourceFactory = feedRepository::feedPagingSource
    )

    val feedPagingData = pager.flow
}

يتم تلخيص مزايا وعيوب المزامنة المستندة إلى السحب في الجدول أدناه:

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

المزامنة المستندة إلى الدفع

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

المزامنة المستندة إلى الدفع
الشكل 8: المزامنة المستندة إلى الدفع: تُعلم الشبكة التطبيق عند تغيير البيانات واستجابة التطبيق من خلال جلب البيانات التي تم تغييرها

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

class UserDataRepository(...) {

    suspend fun synchronize() {
        val userData = networkDataSource.fetchUserData()
        localDataSource.saveUserData(userData)
    }
}

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

يتم تلخيص مزايا وعيوب المزامنة المستندة إلى الدفع في الجدول أدناه:

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

المزامنة المختلطة

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

يعتمد اختيار المزامنة بلا اتصال بالإنترنت أولاً على متطلبات المنتج والبنية الأساسية الفنية المتاحة.

حل النزاعات

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

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

مرات الفوز في آخر كتابة

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

آخر عملية كتابة تفوز بحل النزاع
الشكل 9: "آخر كتابات فوز". يتم تحديد مصدر الحقيقة للبيانات من خلال الكيان الأخير الذي يكتب البيانات.

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

WorkManager في التطبيقات التي تعمل بلا اتصال بالإنترنت أولاً

في كل من استراتيجيات القراءة والكتابة المذكورة أعلاه، كان هناك منفعتان شائعتان:

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

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

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

ويتم توضيح ما سبق في الرسم التخطيطي التالي:

مزامنة البيانات في تطبيق Now في Android
الشكل 10: مزامنة البيانات في تطبيق Now في Android

يتبع عمل المزامنة في قائمة الانتظار مع WorkManager تحديده على أنّه عمل فريد باستخدام KEEP ExistingWorkPolicy:

class SyncInitializer : Initializer<Sync> {
   override fun create(context: Context): Sync {
       WorkManager.getInstance(context).apply {
           // Queue sync on app startup and ensure only one
           // sync worker runs at any time
           enqueueUniqueWork(
               SyncWorkName,
               ExistingWorkPolicy.KEEP,
               SyncWorker.startUpSyncWork()
           )
       }
       return Sync
   }
}

حيثما يتم تحديد SyncWorker.startupSyncWork() على النحو التالي:


/**
 Create a WorkRequest to call the SyncWorker using a DelegatingWorker.
 This allows for dependency injection into the SyncWorker in a different
 module than the app module without having to create a custom WorkManager
 configuration.
*/
fun startUpSyncWork() = OneTimeWorkRequestBuilder<DelegatingWorker>()
    // Run sync as expedited work if the app is able to.
    // If not, it runs as regular work.
   .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
   .setConstraints(SyncConstraints)
    // Delegate to the SyncWorker.
   .setInputData(SyncWorker::class.delegatedData())
   .build()

val SyncConstraints
   get() = Constraints.Builder()
       .setRequiredNetworkType(NetworkType.CONNECTED)
       .build()

على وجه التحديد، تتطلّب السمة Constraints التي حدّدتها SyncConstraints أن تكون السمة NetworkType NetworkType.CONNECTED. أي أنه ينتظر حتى تتوفر الشبكة قبل تشغيلها.

عند توفُّر الشبكة، يستنزف العامل قائمة انتظار العمل الفريدة التي تحدّدها SyncWorkName من خلال التفويض بحالات Repository المناسبة. وإذا تعذّرت المزامنة، يتم عرض الطريقة doWork() مع الخطأ Result.retry(). ستعيد أداة WorkManager محاولة المزامنة تلقائيًا باستخدام التراجع الأسي. وبخلاف ذلك، سيتم عرض عملية إكمال المزامنة Result.success().

class SyncWorker(...) : CoroutineWorker(appContext, workerParams), Synchronizer {

    override suspend fun doWork(): Result = withContext(ioDispatcher) {
        // First sync the repositories in parallel
        val syncedSuccessfully = awaitAll(
            async { topicRepository.sync() },
            async { authorsRepository.sync() },
            async { newsRepository.sync() },
        ).all { it }

        if (syncedSuccessfully) Result.success()
        else Result.retry()
    }
}

عيّنات

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