التطبيق الذي يعمل بلا إنترنت أولاً هو تطبيق يمكنه تنفيذ جميع وظائفه الأساسية أو مجموعة فرعية مهمة منها بدون الاتصال بالإنترنت. أي أنه يمكنه تنفيذ بعض أو كل منطق الأعمال الخاص به في وضع عدم الاتصال.
تبدأ اعتبارات إنشاء تطبيق يعمل بلا إنترنت أولاً في طبقة البيانات، التي تتيح الوصول إلى بيانات التطبيق ومنطق النشاط التجاري. من حين لآخر، قد يحتاج التطبيق إلى إعادة تحميل هذه البيانات من مصادر خارجية عن الجهاز. عند القيام بذلك، قد يحتاج إلى استدعاء موارد الشبكة للبقاء على اطلاع دائم.
لا يمكن ضمان توفّر الشبكة دائمًا. غالبًا ما تواجه الأجهزة فترات من الاتصال بالشبكة المتقطع أو البطيء. قد يواجه المستخدمون ما يلي:
- معدّل نقل بيانات الإنترنت محدود
- انقطاعات الاتصال المؤقتة، مثل تلك التي تحدث في المصعد أو النفق
- الوصول إلى البيانات من حين لآخر، مثل الأجهزة اللوحية التي تتصل بشبكة Wi-Fi فقط
وبغض النظر عن السبب، غالبًا ما يكون من الممكن أن يعمل التطبيق بشكل كافٍ في هذه الظروف. لضمان عمل تطبيقك بشكل صحيح بلا إنترنت، يجب أن يكون قادرًا على إجراء ما يلي:
- تظل قابلة للاستخدام بدون اتصال شبكة موثوق به
- عرض البيانات المحلية للمستخدمين على الفور بدلاً من انتظار اكتمال طلب الشبكة الأول أو تعذّره
- جلب البيانات بطريقة تراعي حالة البطارية والبيانات، على سبيل المثال، من خلال طلب جلب البيانات في الظروف المثالية فقط، مثل أثناء الشحن أو عند الاتصال بشبكة Wi-Fi
غالبًا ما يُطلق على التطبيق الذي يستوفي هذه المعايير اسم "تطبيق يعمل بلا إنترنت أولاً".
تصميم تطبيق يعمل بلا إنترنت أولاً
عند تصميم تطبيق يعمل بلا إنترنت أولاً، ابدأ في طبقة البيانات والعمليتَين الرئيسيَّتَين اللتَين يمكنك تنفيذهما على بيانات التطبيق:
- عمليات القراءة: استرداد البيانات لاستخدامها في أجزاء أخرى من التطبيق، مثل عرض المعلومات للمستخدم في Compose، يمكنك عادةً تحقيق ذلك من خلال مراقبة الحالة. عندما تراقب واجهة المستخدم مصدر البيانات المحلي كحالة، تعكس الشاشة أحدث البيانات المحلية تلقائيًا.
- عمليات الكتابة: الاحتفاظ ببيانات المستخدمين التي تم إدخالها لاستردادها لاحقًا في Compose، يمكنك تحقيق ذلك عادةً باستخدام الأحداث والإجراءات المرسَلة من واجهة المستخدم إلى ViewModel.
المستودعات في طبقة البيانات مسؤولة عن دمج مصادر البيانات لتوفير بيانات التطبيق. في التطبيقات التي تعتمد على وضع عدم الاتصال بالإنترنت، يجب أن يكون هناك مصدر بيانات واحد على الأقل لا يحتاج إلى الاتصال بالشبكة لتنفيذ المهام الأكثر أهمية. تتمثل إحدى هذه المهام الحاسمة في قراءة البيانات.
نمذجة البيانات في تطبيق يعمل بلا إنترنت أولاً
يحتوي التطبيق الذي يعمل بلا إنترنت أولاً على مصدرَي بيانات على الأقل لكل مستودع يستخدم موارد الشبكة:
- مصدر البيانات المحلي
- مصدر بيانات الشبكة
مصدر البيانات المحلي
مصدر البيانات المحلي هو المصدر الموثوق الأساسي للتطبيق، ويجب أن يكون المصدر الحصري لأي بيانات تقرأها الطبقات العليا من التطبيق. يضمن ذلك اتساق البيانات بين حالات الاتصال. غالبًا ما يتم دعم مصدر البيانات المحلي بمساحة تخزين محفوظة على القرص. في ما يلي بعض الوسائل الشائعة لحفظ البيانات على القرص:
- مصادر البيانات المنظَّمة، مثل قواعد البيانات الارتباطية مثل Room
- مصادر البيانات غير المنظَّمة، مثل Protocol Buffers مع DataStore
- الملفات البسيطة
مصدر بيانات الشبكة
مصدر بيانات الشبكة هو الحالة الفعلية للتطبيق. في أفضل الأحوال، تتم مزامنة مصدر البيانات المحلي مع مصدر بيانات الشبكة. يمكن أن يتأخر مصدر البيانات المحلي أيضًا عن مصدر بيانات الشبكة، وفي هذه الحالة يجب تحديث التطبيق عند الاتصال بالإنترنت مرة أخرى. وعلى العكس، قد يتأخر مصدر بيانات الشبكة عن مصدر البيانات المحلي إلى أن يتمكن التطبيق من تحديثه عند عودة الاتصال. يجب ألا تتواصل طبقات النطاق وواجهة المستخدم في التطبيق مباشرةً مع طبقة الشبكة. تقع على عاتق repository المستضيف مسؤولية التواصل مع هذا المصدر واستخدامه لتعديل مصدر البيانات المحلي.
المزامنة المخصّصة مقابل المزامنة المُدارة
يمكن إنشاء طبقة بيانات تعتمد على وضع عدم الاتصال بالإنترنت بطريقتَين أساسيتَين:
- مجموعة مخصّصة للمزامنة: يمكنك الاحتفاظ بقواعد البيانات المحلية باستخدام Room أو الملفات في مساحة التخزين المحلية، وإنشاء برامج مخصّصة لشبكة الاتصال، وجدولة مهام التسوية باستخدام WorkManager. يتيح هذا النهج أقصى قدر من التحكّم في المخططات وخوارزميات حلّ التعارضات المخصّصة.
- خدمات السحابة الإلكترونية المُدارة: يمكن أن توفّر حلول مثل Cloud Firestore ذاكرة تخزين مؤقت محلية شاملة وبثًا سحابيًا في الوقت الفعلي. تتولى حزمة تطوير البرامج (SDK) إدارة ذاكرة التخزين المؤقت للقرص، وقائمة انتظار الكتابة بلا إنترنت، والمزامنة الثنائية الاتجاه تلقائيًا، وتقدّم مصادر بيانات في الوقت الفعلي مباشرةً إلى مستودعك. بالنسبة إلى الواجهات الخلفية العلائقية في PostgreSQL، تنشئ أداة Firebase SQL Connect حِزم تطوير برامج (SDK) لعملاء Kotlin تمنع أخطاء الكتابة وتتكامل بسلاسة مع مستودعاتك. بالنسبة إلى البيانات غير المنظَّمة (مثل الوسائط أو الملفات الكبيرة أو النُسخ الاحتياطية)، توفّر مساحة التخزين السحابي لـ Firebase مساحة تخزين للعناصر المستضافة على السحابة الإلكترونية مع عمليات تحميل وتنزيل تتوقف مؤقتًا وتُستأنف تلقائيًا عند انقطاع الاتصال بالشبكة.
عرض الموارد
يمكن أن تختلف مصادر البيانات المحلية ومصادر بيانات الشبكة بشكل أساسي في كيفية قراءة تطبيقك لها والكتابة إليها. يمكن أن يكون الاستعلام عن مصدر البيانات المحلّية سريعًا ومرنًا، مثل استخدام استعلامات لغة الاستعلامات البنيوية (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، عندما يحدث فشل أثناء القراءة، تظهر حالة خطأ. عادةً، يمكنك تحقيق 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
)
}
مصدر بيانات الشبكة
إذا حدثت أخطاء أثناء قراءة البيانات من مصدر بيانات على الشبكة، يحتاج التطبيق إلى استخدام أسلوب إرشادي لإعادة محاولة جلب البيانات. تشمل الأساليب الإرشادية الشائعة ما يلي:
الرقود الأسي الثنائي
في التراجع الأسي، يستمر التطبيق في محاولة القراءة من مصدر بيانات الشبكة بفواصل زمنية متزايدة حتى ينجح، أو تفرض شروط أخرى وجوب توقفه.
تشمل معايير تقييم ما إذا كان التطبيق يستمر في التراجع ما يلي:
- نوع الخطأ الذي أشار إليه مصدر بيانات الشبكة على سبيل المثال، أعِد محاولة إجراء طلبات الشبكة التي تعرض خطأً يشير إلى عدم توفّر اتصال. لا تُعِد محاولة إرسال طلبات HTTP غير المصرَّح بها إلى أن تتوفّر بيانات الاعتماد المناسبة.
- الحد الأقصى المسموح به لعدد المحاولات
مراقبة الاتصال بالشبكة
في هذا النهج، يتم وضع طلبات القراءة في قائمة انتظار حتى يتأكد التطبيق من إمكانية الاتصال بمصدر بيانات الشبكة. بعد إنشاء الاتصال، تتم إزالة طلب القراءة من قائمة الانتظار، وقراءة البيانات، وتحديث مصدر البيانات المحلي. على Android، يمكن الاحتفاظ بقائمة الانتظار هذه باستخدام قاعدة بيانات Room، وتفريغها كعمل مستمر باستخدام WorkManager.
الكتابة
على الرغم من أنّ الطريقة المقترَحة لقراءة البيانات في تطبيق يعمل بلا إنترنت أولاً هي استخدام الأنواع القابلة للتتبّع، فإنّ واجهات برمجة التطبيقات المكافئة لواجهات برمجة التطبيقات للكتابة هي واجهات برمجة التطبيقات غير المتزامنة مثل الدوال المعلّقة. يساعد ذلك في تجنُّب حظر سلسلة واجهة المستخدم، كما يساعد في معالجة الأخطاء لأنّ عمليات الكتابة في التطبيقات التي تعمل بلا إنترنت أولاً قد تفشل عند تجاوز حدود الشبكة.
interface UserDataRepository {
/**
* Updates the bookmarked status for a news resource
*/
suspend fun updateNewsResourceBookmark(newsResourceId: String, bookmarked: Boolean)
}
في المقتطف السابق، واجهة برمجة التطبيقات غير المتزامنة التي تم اختيارها هي Coroutines لأنّ الطريقة يتم تعليقها.
استراتيجيات الكتابة
عند كتابة البيانات في التطبيقات التي تعتمد على وضع عدم الاتصال أولاً، هناك ثلاث استراتيجيات يجب مراعاتها. يعتمد اختيارك على نوع البيانات التي تتم كتابتها ومتطلبات التطبيق:
الكتابة على الإنترنت فقط
محاولة كتابة البيانات عبر حدود الشبكة إذا نجحت، فقم بتحديث مصدر البيانات المحلي؛ وإلا، فقم برمي استثناء واتركه للمتصل للرد بشكل مناسب.
غالبًا ما تُستخدم هذه الاستراتيجية في معاملات الكتابة التي يجب أن تتم على الإنترنت في الوقت الفعلي تقريبًا، مثل الحوالات المصرفية. بما أنّ عمليات الكتابة قد تفشل، من الضروري غالبًا إبلاغ المستخدم بفشل عملية الكتابة أو منعه من محاولة كتابة البيانات في المقام الأول. فيما يلي بعض الاستراتيجيات التي يمكنك استخدامها في هذه السيناريوهات:
- إذا كان التطبيق يتطلّب إمكانية الوصول إلى الإنترنت لكتابة البيانات، يمكنك اختيار عدم عرض واجهة مستخدم تتيح للمستخدم كتابة البيانات، أو يمكنك على الأقل إيقافها.
- يمكنك استخدام
AlertDialogلا يمكن للمستخدم إغلاقه، أوSnackbar، لإعلام المستخدم بأنّه غير متصل بالإنترنت.
عمليات الكتابة في قائمة الانتظار
عندما يكون لديك كائن تريد كتابته، أدخِله في قائمة انتظار. عندما يعود التطبيق إلى الاتصال بالإنترنت، يتم تفريغ قائمة الانتظار باستخدام خوارزمية الرقود الأسي الثنائي. على أجهزة Android، يُعدّ استنزاف قائمة الانتظار بلا إنترنت عملاً مستمرًا يتم تفويضه غالبًا إلى WorkManager.
يُعدّ هذا النهج خيارًا جيدًا في الحالات التالية:
- وليس من الضروري أن تتم كتابة البيانات على الشبكة.
- المعاملة ليست حساسة للوقت.
- وليس من الضروري إعلام المستخدم في حال تعذّر تنفيذ العملية.
تشمل حالات استخدام هذا النهج أحداث الإحصاءات والتسجيل.
عمليات الكتابة البطيئة
اكتب إلى مصدر البيانات المحلي أولاً، ثم ضع الكتابة في قائمة الانتظار لإعلام الشبكة في أقرب فرصة. هذا ليس بالأمر السهل لأنّه قد تحدث تعارضات بين الشبكة ومصادر البيانات المحلية عندما يعود التطبيق إلى الاتصال بالإنترنت. يقدم القسم التالي حول حل النزاعات مزيدًا من التفاصيل.
هذا النهج هو الخيار الصحيح عندما تكون البيانات مهمة للتطبيق. على سبيل المثال، في تطبيق قائمة المهام الذي يعمل بلا إنترنت أولاً، من الضروري تخزين أي مهام يضيفها المستخدم بلا إنترنت محليًا لتجنب خطر فقدان البيانات.
المزامنة وحلّ التعارضات
عندما يستعيد التطبيق الذي يعمل بلا إنترنت أولاً إمكانية الاتصال، يحتاج إلى تسوية البيانات في مصدر البيانات المحلي مع البيانات الموجودة في مصدر بيانات الشبكة. تُعرف هذه العملية باسم المزامنة. هناك طريقتان رئيسيتان يمكن للتطبيق من خلالهما المزامنة مع مصدر بيانات الشبكة:
- المزامنة المستندة إلى السحب
- المزامنة المستندة إلى الإشعارات الفورية
المزامنة المستندة إلى السحب
في المزامنة المستندة إلى السحب، يتواصل التطبيق مع الشبكة لقراءة أحدث بيانات التطبيق عند الطلب. من الأساليب الإرشادية الشائعة لهذا النهج هو الأسلوب المستند إلى التنقّل، حيث يجلب التطبيق البيانات فقط قبل تقديمها للمستخدم.
يعمل هذا النهج بشكل أفضل عندما يتوقع التطبيق فترات قصيرة إلى متوسطة من عدم الاتصال بالشبكة. ويرجع ذلك إلى أنّ تحديث البيانات يتم بشكل انتهازي، كما أنّ فترات انقطاع إمكانية الاتصال الطويلة تزيد من احتمال محاولة المستخدم زيارة وجهات التطبيق باستخدام ذاكرة تخزين مؤقت قديمة أو فارغة.
لنفترض أنّ لديك تطبيقًا يتم فيه استخدام رموز مميزة للصفحات من أجل جلب العناصر في قائمة تمرير لا نهائية لشاشة معيّنة. قد يتواصل التنفيذ مع الشبكة بشكل كسول، ويحفظ البيانات في مصدر البيانات المحلي، ثم يقرأ من مصدر البيانات المحلي لتقديم المعلومات مرة أخرى إلى المستخدم. في حالة عدم وجود اتصال بالشبكة، قد يطلب المستودع بيانات من مصدر البيانات المحلي وحده. هذا هو النمط الذي تستخدمه
مكتبة 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. |
| لا يتم جلب البيانات غير المطلوبة مطلقًا. | لا يتوافق بشكل جيد مع البيانات ذات الصلة لأنّ النموذج الذي يتم سحبه يجب أن يكون مكتفيًا ذاتيًا. إذا كان النموذج الذي تتم مزامنته يعتمد على نماذج أخرى يتم جلبها لملء نفسه، فإن مشكلة استخدام البيانات الكثيفة المذكورة سابقًا تصبح أكثر أهمية. بالإضافة إلى ذلك، يمكن أن يؤدي ذلك إلى حدوث تبعيات بين مستودعات النموذج الرئيسي ومستودعات النموذج المُدمَج. |
المزامنة المستندة إلى الإشعارات الفورية
في المزامنة المستندة إلى الإرسال، يحاول مصدر البيانات المحلي محاكاة مجموعة نسخ متماثلة من مصدر بيانات الشبكة بأفضل ما يمكنه. ويجلب بشكل استباقي كمية مناسبة من البيانات عند بدء التشغيل الأول لتعيين خط الأساس. بعد ذلك، يعتمد على الإشعارات من الخادم لتنبيهه عندما تكون هذه البيانات قديمة.
عند تلقّي إشعار البيانات القديمة، يتواصل التطبيق مع الشبكة لتحديث البيانات التي تم وضع علامة عليها كقديمة فقط. يتم تفويض هذه المهمة إلى
Repository، الذي يتواصل مع مصدر بيانات الشبكة ويحفظ البيانات
التي تم جلبها إلى مصدر البيانات المحلي. بما أنّ المستودع يعرض بياناته باستخدام أنواع قابلة للتتبّع، يتم إعلام القرّاء بأي تغييرات.
class UserDataRepository(...) {
suspend fun synchronize() {
val userData = networkDataSource.fetchUserData()
localDataSource.saveUserData(userData)
}
}
في هذا النهج، يعتمد التطبيق بشكل أقل على مصدر بيانات الشبكة ويمكنه العمل بدونه لفترات طويلة. ويوفّر إمكانية الوصول للقراءة والكتابة عند عدم الاتصال بالإنترنت لأنّه يفترض أنّه يتضمّن أحدث المعلومات من مصدر بيانات الشبكة على الجهاز.
يتم تلخيص مزايا وعيوب المزامنة المستندة إلى الدفع في الجدول التالي:
| المزايا | العيوب |
|---|---|
| يمكن أن يظل التطبيق غير متصل بالإنترنت إلى أجل غير مسمى. | إنّ تحديد إصدارات البيانات لحلّ التعارضات ليس أمرًا بسيطًا. |
| الحد الأدنى لاستخدام البيانات يجلب التطبيق البيانات التي تم تغييرها فقط. | يجب أن تأخذ في الاعتبار مخاوف الكتابة أثناء المزامنة. |
| تعمل بشكل جيد مع البيانات الارتباطية. كل مستودع مسؤول عن جلب البيانات للنموذج الذي يدعمه فقط. | يجب أن يتيح مصدر بيانات الشبكة المزامنة. |
المزامنة المختلطة
تستخدم بعض التطبيقات نهجًا مختلطًا يعتمد على السحب أو الدفع حسب البيانات. على سبيل المثال، قد يستخدم تطبيق تواصل اجتماعي المزامنة المستندة إلى السحب لجلب خلاصة المتابعة للمستخدم عند الطلب بسبب معدل تكرار تحديثات الخلاصة المرتفع. قد يختار التطبيق نفسه استخدام المزامنة المستندة إلى الإشعارات الفورية للبيانات المتعلقة بالمستخدم الذي سجّل الدخول، بما في ذلك اسم المستخدم وصورة الملف الشخصي وما إلى ذلك.
في النهاية، يعتمد اختيار المزامنة بلا إنترنت أولاً على متطلبات المنتج والبنية الأساسية التقنية المتاحة.
حل النزاعات
إذا كان التطبيق، عندما يكون غير متصل بالإنترنت، يكتب البيانات محليًا بشكل غير متوافق مع مصدر بيانات الشبكة، فيجب عليك حل التعارض قبل حدوث المزامنة.
غالبًا ما يتطلب حلّ التعارضات استخدام الإصدارات. يحتاج التطبيق إلى إجراء بعض عمليات مسك الدفاتر لتتبع وقت حدوث التغييرات، حتى يتمكن من تمرير البيانات الوصفية إلى مصدر بيانات الشبكة. بعد ذلك، يكون مصدر بيانات الشبكة مسؤولاً عن توفير المصدر المطلق للمعلومات. هناك العديد من الاستراتيجيات التي يمكن استخدامها لحلّ التعارضات، وذلك حسب احتياجات التطبيق. بالنسبة إلى التطبيقات المتوافقة مع الأجهزة الجوّالة، يُعدّ النهج الشائع هو "الاحتفاظ بآخر تعديل".
آخر عملية كتابة تفوز
في هذا النهج، ترفق الأجهزة البيانات الوصفية للطابع الزمني بالبيانات التي تكتبها على الشبكة. وعندما يتلقّى مصدر بيانات الشبكة هذه البيانات، يتجاهل أي بيانات أقدم من حالته الحالية ويقبل البيانات الأحدث من حالته الحالية.
في الشكل 9، يكون كلا الجهازين غير متصلين بالإنترنت، وتتم مزامنتهما في البداية مع مصدر بيانات الشبكة. أثناء عدم الاتصال بالإنترنت، يكتب كلاهما البيانات محليًا ويتتبعان وقت كتابة بياناتهما. عندما يعود كلاهما إلى الإنترنت ويتزامنان مع مصدر بيانات الشبكة، تحل الشبكة التعارض عن طريق الاحتفاظ بالبيانات من الجهاز "ب" لأنه كتب بياناته لاحقًا.
WorkManager في التطبيقات التي تعمل بلا إنترنت أولاً
في كل من استراتيجيات القراءة والكتابة التي تم تناولها سابقًا، هناك نوعان شائعان من الأدوات المساعدة:
- قوائم الانتظار
- عمليات القراءة: تُستخدَم لتأجيل عمليات القراءة إلى أن يتوفّر اتصال بالشبكة.
- عمليات الكتابة: تُستخدَم لتأجيل عمليات الكتابة إلى حين توفّر اتصال بالشبكة، ولإعادة وضع عمليات الكتابة في قائمة الانتظار لإعادة المحاولة.
- أدوات مراقبة الاتصال بالشبكة
- عمليات القراءة: تُستخدم كإشارة لتفريغ قائمة انتظار القراءة عندما يكون التطبيق متصلاً، وللمزامنة.
- عمليات الكتابة: تُستخدَم كإشارة لتفريغ قائمة انتظار عمليات الكتابة عندما يكون التطبيق متصلاً، وللمزامنة.
كلتا الحالتَين هما مثالان على العمل المستمر الذي تتفوّق فيه واجهة WorkManager.
على سبيل المثال، في نموذج تطبيق Now in Android، يتم استخدام WorkManager كقائمة انتظار للقراءة ومراقبة للشبكة عند مزامنة مصدر البيانات المحلي. عند بدء التشغيل، ينفّذ التطبيق ما يلي:
- يضع في قائمة الانتظار عمل مزامنة القراءة للتأكد من وجود تكافؤ بين مصدر البيانات المحلي ومصدر بيانات الشبكة.
- يتم تفريغ قائمة انتظار مزامنة القراءة وبدء المزامنة عندما يكون التطبيق متصلاً بالإنترنت.
- يؤدي هذا الإجراء إلى قراءة البيانات من مصدر بيانات الشبكة باستخدام خوارزمية الرقود الأسي الثنائي.
- يتم الاحتفاظ بنتائج القراءة في مصدر البيانات المحلي وحلّ أي تعارضات تحدث.
- يعرض البيانات من مصدر البيانات المحلي لتستخدمها الطبقات الأخرى في التطبيق.
يتم توضيح هذه الإجراءات في المخطّط البياني التالي:
يتم وضع مهمة المزامنة في قائمة الانتظار باستخدام 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 التالية التطبيقات التي تعمل بلا إنترنت أولاً. يمكنك استكشافها للاطّلاع على هذه الإرشادات عمليًا:
التطبيقات المقترَحة لك
- ملاحظة: يتم عرض نص الرابط عندما يكون JavaScript غير مفعَّل
- إعداد حالة واجهة المستخدم
- طبقة واجهة المستخدم
- طبقة البيانات