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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

مصدر بيانات الشبكة هو الحالة الفعلية للتطبيق. في أفضل الأحوال، تتم مزامنة مصدر البيانات المحلي مع مصدر بيانات الشبكة. قد يتأخر مصدر البيانات المحلية أيضًا عن مصدر بيانات الشبكة، وفي هذه الحالة يجب تحديث التطبيق عند إعادة الاتصال بالإنترنت. في المقابل، قد يتأخر مصدر بيانات الشبكة عن مصدر البيانات المحلي إلى أن يتمكّن التطبيق من تعديله عند استعادة الاتصال. يجب ألا تتواصل طبقات النطاق وواجهة المستخدم في التطبيق مباشرةً مع طبقة الشبكة. وتقع على عاتق 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 قيم Flow لجميع واجهات برمجة التطبيقات للقراءة. يتيح ذلك تحديث القارئات عند تلقّي تحديثات من مصدر بيانات الشبكة. بعبارة أخرى، يتيح ذلك OfflineFirstTopicRepository إرسال التغييرات عندما يصبح مصدر البيانات المحلي غير صالح. لذلك، يجب أن يكون كل قارئ لـ OfflineFirstTopicRepository مستعدًا للتعامل مع تغييرات البيانات التي يمكن أن تحدث عند استعادة اتصال التطبيق بالشبكة. بالإضافة إلى ذلك، يقرأ OfflineFirstTopicRepository البيانات مباشرةً من مصدر البيانات المحلي. ولا يمكنه إعلام القرّاء بالتغييرات في البيانات إلا من خلال تعديل مصدر البيانات المحلي أولاً.

class TopicsViewModel(
    offlineFirstTopicsRepository: OfflineFirstTopicsRepository
) : ViewModel() {

    val topics: StateFlow<List<Topic>> = offlineFirstTopicsRepository.getTopicsStream()
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = emptyList()
        )
}

في تطبيق Jetpack Compose، استخدِم ViewModel لربط طبقة البيانات بواجهة المستخدم. في ViewModel، حوِّل Flow إلى StateFlow باستخدام عامل التشغيل stateIn. بعد ذلك، تجمع العناصر القابلة للإنشاء هذه الحالات باستخدام collectAsStateWithLifecycle() وتدير الاشتراكات تلقائيًا بطريقة تراعي مراحل النشاط.

لمزيد من المعلومات عن collectAsStateWithLifecycle()، راجِع مقالة الحالة وJetpack Compose.

استراتيجيات معالجة الأخطاء

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

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

حاوِل تقليل الأخطاء عند القراءة من مصدر البيانات المحلي. لحماية القراء من الأخطاء، استخدِم عامل التشغيل catch على Flow التي يجمع منها القارئ البيانات.

يمكنك استخدام عامل التشغيل 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()) }
}

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

// Define the LCE UI state
sealed interface AuthorUiState {
    data object Loading : AuthorUiState
    data class Success(val author: Author) : AuthorUiState
    data object Error : AuthorUiState
}

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

    // Observe author information and map to LCE state
    val authorUiState: StateFlow<AuthorUiState> =
        authorsRepository.getAuthorStream(id = authorId)
            .map<Author, AuthorUiState> { author ->
                AuthorUiState.Success(author)
            }
            .catch { emit(AuthorUiState.Error) }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = AuthorUiState.Loading
            )
}

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

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

الرقود الأسي الثنائي

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

قراءة البيانات باستخدام خوارزمية الرقود الأسي الثنائي
الشكل 2: قراءة البيانات باستخدام خوارزمية الرقود الأسي الثنائي

تشمل معايير تقييم ما إذا كان التطبيق يتوقف عن العمل ما يلي:

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

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

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

يكتب

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

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

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

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

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

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

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

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

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

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

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

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

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

يُعدّ هذا النهج خيارًا جيدًا في الحالات التالية:

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

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

عمليات الكتابة المؤجّلة

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

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

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

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

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

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

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

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

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

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

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

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

class UserDataRepository(...) {

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

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

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

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

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

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

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

حلّ النزاعات

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

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

الأولوية للكتابة الأخيرة

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

حلّ التعارضات باستخدام قاعدة &quot;آخر تعديل يفوز&quot;
الشكل 9: "الأولوية لآخر تعديل" يتم تحديد مصدر صحة البيانات من خلال آخر كيان يكتب البيانات.

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

‫WorkManager في التطبيقات التي تعمل بلا اتصال بالإنترنت

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

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

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

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

يوضّح الرسم البياني التالي هذه الإجراءات:

مزامنة البيانات في تطبيق Now in Android
الشكل 10: مزامنة البيانات في تطبيق Now in 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. أي أنّه ينتظر إلى أن تصبح الشبكة متاحة قبل تشغيلها.

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