أنماط المزامنة الخلفية باستخدام Kotlin WorkManager لتطبيقات الميدان
أنماط المزامنة الخلفية باستخدام Kotlin WorkManager لتطبيقات الميدان: اختر نوع العمل المناسب، اضبط القيود، استخدم تراجعًا أسيًا لإعادة المحاولات، وأظهر تقدمًا مرئيًا للمستخدم.

ماذا تعني المزامنة الخلفية الموثوقة لتطبيقات الميدان والعمليات
في تطبيقات الميدان والعمليات، المزامنة ليست "ميزة اختيارية". هي كيف يتحول العمل على الجهاز إلى شيء حقيقي للفريق. عندما تفشل المزامنة يلاحظ المستخدمون ذلك بسرعة: وظيفة مكتملة تبدو "قيد الانتظار"، صور تختفي، أو نفس التقرير يُرفع مرتين ويُنشئ سجلات مكررة.
تكون هذه التطبيقات أصعب من تطبيقات المستهلكين لأن الهواتف تعمل في أسوأ الظروف. الشبكة تتقلب بين LTE، واي‑فاي ضعيف، ولا توجد إشارة. وضع حفظ البطارية يمنع العمل في الخلفية. التطبيق يُغلق، النظام يحدث، والأجهزة تعيد التشغيل أثناء الجولة. إعداد WorkManager موثوق يجب أن يصمد أمام كل ذلك بدون دراما.
الموثوقية عادةً تعني أربعة أشياء:
- متسق في النهاية: قد تصل البيانات متأخرة، لكن تصل دون تدخل يدوي.\n- قابل للاسترداد: إن مات التطبيق أثناء الرفع، يستمر التشغيل التالي بأمان.\n- قابل للملاحظة: المستخدمون والدعم يمكنهم معرفة ما يحدث وما المتعثر.\n- غير مدمِّر: إعادة المحاولات لا تُنشئ سجلات مكررة أو تفسد الحالة.
"تشغيل الآن" يناسب الإجراءات الصغيرة التي يطلقها المستخدم والتي يجب أن تنتهي قريبًا (مثلاً إرسال تحديث حالة واحد قبل أن يغلق المستخدم المهمة). "الانتظار" يناسب الأعمال الأثقل مثل رفع الصور، تحديثات الدُفعات، أو أي شيء مرجح أن يستنزف البطارية أو يفشل على الشبكات السيئة.
مثال: مفتش يرسل نموذجًا مع 12 صورة في قبو بلا إشارة. مزامنة موثوقة تخزن كل شيء محليًا، وتعَلِّم السجل بأنه مُدرج في الطابور، وترفعه لاحقًا عندما يتوفر اتصال حقيقي، دون أن يعيد المفتش تنفيذ العمل.
اختر لبنات WorkManager المناسبة
ابدأ بتحديد أصغر وحدة عمل واضحة. هذا القرار يؤثر على الموثوقية أكثر من أي منطق إعادة محاولات ذكي لاحقًا.
عمل لمرة واحدة مقابل عمل دوري
استخدم OneTimeWorkRequest للعمل الذي يجب أن يحدث لأن شيئًا تغيّر: حفظ نموذج جديد، انتهى ضغط صورة، أو ضغط المستخدم زر المزامنة. أدرجه فورًا (مع القيود) ودع WorkManager ينفذه عندما يكون الجهاز جاهزًا.
استخدم PeriodicWorkRequest للصيانة المستمرة، مثل "التحقق من التحديثات" الدوري أو تنظيف ليلي. العمل الدوري ليس دقيقًا. له فترة دنيا ويمكن أن ينزلق اعتمادًا على قواعد البطارية والنظام، لذا لا يجب أن يكون طريقك الوحيد للرفع المهم.
نمط عملي هو استخدام عمل لمرة واحدة لـ"يجب المزامنة قريبًا"، والعمل الدوري كشبكة أمان.
اختيار Worker، CoroutineWorker، أو RxWorker
إن كتبتم بـKotlin وتستخدمون دوال suspend، ففضّلوا CoroutineWorker. يبقي الكود قصيرًا ويجعل الإلغاء يتصرّف كما تتوقعون.
Worker يناسب الكود البسيط المحظور، لكن يجب الحذر من الحظر الطويل.\n
RxWorker منطقي فقط إذا كان تطبيقكم يستخدم RxJava بكثافة؛ وإلا فهو تعقيد زائد.
ربط خطوات أو تشغيل عامل واحد بمراحل؟
الربط ممتاز عندما يمكن لكل خطوة أن تنجح أو تفشل بشكل مستقل، وتريدون إعادة محاولات منفصلة وسجلات أوضح. عامل واحد بمراحل قد يكون أفضل عندما تشارك الخطوات بيانات ويجب معاملتها كمعاملة واحدة.
قاعدة بسيطة:
- اربط عندما يكون للخطوات قيود مختلفة (رفع عبر Wi‑Fi فقط، ثم مكالمة API خفيفة).\n- استخدم عاملًا واحدًا عندما تحتاج إلى مزامنة "الكل أو لا شيء".
يضمن WorkManager أن العمل مخزن دائمًا، ويمكنه الصمود أمام موت العملية وإعادة التشغيل، ويحترم القيود. لا يضمن توقيتًا دقيقًا، أو تنفيذًا فوريًا، أو التشغيل بعد أن يوقف المستخدم التطبيق بالقوة. إنكم تبنون تطبيق ميدان على Android، صمموا المزامنة بحيث تكون التأخيرات آمنة ومتوقعة.
اجعل المزامنة آمنة: قابلة للتكرار، تدريجية، وقابلة للاستئناف
تطبيق ميدان سيُعيد تشغيل العمل. الهواتف تفقد الإشارة، والنظام يقتل العمليات، والمستخدمون يضغطون زر المزامنة مرتين لأنهم لم يروا نتيجة. إن لم تكن مزامنتكم آمنة للتكرار، ستحصلون على سجلات مكررة، تحديثات مفقودة، أو محاولات لانهائية.
ابدأ بجعل كل استدعاء للخادم آمنًا للتنفيذ مرتين. أبسط نهج هو مفتاح عدم التكرار لكل عنصر (مثل UUID مخزن مع السجل المحلي) يتعامل معه الخادم كـ"نفس الطلب، نفس النتيجة". إن لم تستطع تغيير الخادم، استخدم مفتاحًا طبيعيًا ثابتًا وواجهة upsert، أو أدرج رقم إصدار حتى يرفض الخادم التحديثات القديمة.
تتبع الحالة المحلية صراحة حتى يستطيع العامل الاستئناف بعد تعطل دون تخمين. آلة حالات بسيطة غالبًا ما تكفي:
- queued\n- uploading\n- uploaded\n- needs-review\n- failed-temporary
اجعل المزامنة تدريجية. بدلًا من "مزامنة كل شيء" خزن مؤشرًا مثل lastSuccessfulTimestamp أو رمز يصدره الخادم. اقرأ صفحة صغيرة من التغييرات، طبقها، ثم قدّم المؤشر فقط بعد أن تُسجّل الدفعة محليًا بالكامل. دفعات صغيرة (مثل 20-100 عنصر) تقلل من انتهاء المهلات، تجعل التقدّم مرئيًا، وتحد من كمية العمل الذي تعيد تكراره بعد مقاطعة.
اجعل الرفع قابلًا للاستئناف أيضًا. للصور أو الحمولات الكبيرة، احفظ URI الملف وبيانات وصفية للرفع، ولا تعلّم العنصر كمرفوع إلا بعد تأكيد الخادم. إن عاد العامل للعمل، يستمر من الحالة المعروفة الأخيرة بدلًا من البدء من الصفر.
مثال: تقني يملأ 12 نموذجًا ويُرفق 8 صور تحت الأرض. عند استعادة الاتصال، يرفع العامل بالدفعات، لكل نموذج مفتاح عدم تكرار، ويتقدم مؤشر المزامنة فقط بعد نجاح كل دفعة. إن مات التطبيق في المنتصف، يعيد تشغيل العامل استكمال العناصر المتبقية دون تكرار أي شيء.
قيود تطابق ظروف الأجهزة الواقعية
القيود هي حواجز تحمي من استنزاف البطاريات، استهلاك باقات البيانات، أو الفشل في أسوأ لحظة. تريدون قيودًا تعكس كيف تتصرف الأجهزة في الميدان، لا كيف تتصرف على مكتبكم.
ابدأ بمجموعة صغيرة تحمي المستخدمين لكنها تسمح للعمل بأن يُنفّذ أغلب الأيام. قاعدة عملية: تطلب اتصالًا بالشبكة، تتجنب التشغيل عند انخفاض البطارية، وتتجنب التشغيل عند امتلاء التخزين. أضف "عند الشحن" فقط إن كان العمل ثقيلًا وغير حساس للوقت، لأن العديد من أجهزة الميدان لا تُوصَل بالكهرباء أثناء الدوام.
الإفراط في وضع القيود سبب شائع لشكوى "المزامنة لا تعمل أبدًا". إن طلبت Wi‑Fi غير مراقب، والشحن، وبطارية غير منخفضة، فقد طلبت لحظة مثالية قد لا تحدث. إن كان العمل مطلوبًا اليوم، من الأفضل تشغيل أعمال أصغر بشكل متكرر بدلاً من الانتظار لظروف مثالية.
البوابات الحبيسة (captive portals) مشكلة واقعية أخرى: الهاتف يقول متصل، لكن يجب على المستخدم الضغط "قبول" على صفحة Wi‑Fi في فندق أو شبكة عامة. WorkManager لا يكتشف هذه الحالة بموثوقية. عاملها على أنها فشل عادي: حاول المزامنة، انتهِ سريعًا، وأعد المحاولة لاحقًا. واحتفظ برسالة بسيطة داخل التطبيق مثل "متصل بالواي‑فاي لكن لا يوجد إنترنت" عندما يمكنك اكتشاف ذلك أثناء الطلب.
استخدم قيودًا مختلفة للرفع الصغير مقابل الكبير حتى يبقى التطبيق مستجيبًا:
- الحمولات الصغيرة (إشارات الحالة، بيانات النموذج): أي شبكة، والبطارية ليست منخفضة.\n- الحمولات الكبيرة (صور، فيديوهات، حزم خرائط): شبكة غير مراقبة عندما يكون ذلك ممكنًا، وفكر في الشحن.
مثال: تقني يحفظ نموذجًا مع صورتين. أرسل حقول النموذج على أي اتصال، لكن ضَع رفع الصور في الطابور لانتظار Wi‑Fi أو لحظة أفضل. المكتب يرى المهمة سريعًا، والجهاز لا يستهلك بيانات الهاتف لرفع الصور في الخلفية.
إعادة المحاولات بتراجع أسي دون إزعاج المستخدمين
إعادة المحاولات هي المكان الذي تجعل تطبيقات الميدان تبدو هادئة أو محطمة. اختر سياسة تراجع تتطابق مع نوع الفشل المتوقع.
التراجع الأسي عادةً هو الافتراضي الآمن لشبكات ضعيفة. يزيد بسرعة وقت الانتظار حتى لا تهاجم الخادم أو تستنزف البطارية عند سوء التغطية. التراجع الخطي قد يناسب مشاكل قصيرة مؤقتة (مثل VPN متقلب)، لكنه يعيد المحاولة كثيرًا في مناطق الإشارة الضعيفة.
اتخذ قرار إعادة المحاولة بناءً على نوع الفشل، لا فقط "حدث خطأ ما". مجموعة قواعد بسيطة مفيدة:
- مهلة شبكة، 5xx، DNS، لا وجود للاتصال:
Result.retry()\n- انتهاء صلاحية المصادقة (401): حدِّث التوكن مرة واحدة، ثم افشل واطلب من المستخدم تسجيل الدخول\n- أخطاء التحقق أو 4xx (طلب خاطئ):Result.failure()مع رسالة واضحة للدعم\n- تعارض (409) لعناصر أُرسلت بالفعل: اعتبره نجاحًا إن كانت مزامنتك غير متكررة
حدّ من الضرر حتى لا تدخل خطأ دائم في حلقة لا نهائية. حدد عدد محاولات أقصى، وبعدها توقف واظهر رسالة هادئة وقابلة للتنفيذ (ليس إشعارًا متكررًا).
يمكنك أيضًا تغيير السلوك مع نمو عدد المحاولات. على سبيل المثال، بعد فشلين، أرسل دفعات أصغر أو أجل الرفع الكبير حتى السحب الناجح التالي.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS
)
.build()
// in doWork()
if (runAttemptCount >= 5) return Result.failure()
return Result.retry()
هذا يحافظ على أخلاقية إعادة المحاولات: نوبات إيقاظ أقل، انقطاعات أقل للمستخدم، واسترداد أسرع عندما يعود الاتصال.
تقدم مرئي للمستخدم: إشعارات، عمل في المقدمة، والحالة
تطبيقات الميدان غالبًا ما تزامن عندما لا يتوقع المستخدم ذلك: في قبو، على شبكة بطيئة، وبطارية شبه ميتة. إن كانت المزامنة تؤثر فيما ينتظره المستخدم (رفع، إرسال تقارير، دفعات صور)، اجعلها مرئية وسهلة الفهم. العمل الصامت في الخلفية ممتاز للتحديثات الصغيرة والسريعة. أي شيء أطول يجب أن يبدو صادقًا.
متى يلزم التشغيل في المقدمة
استخدم التنفيذ في المقدمة عندما تكون المهمة طويلة، حساسة للوقت، أو مرتبطة بوضوح بعمل المستخدم. على Android الحديث، قد تتوقف عمليات الرفع الكبيرة أو تتأخر ما لم تشغلها كمهمة في المقدمة. في WorkManager، هذا يعني إرجاع ForegroundInfo حتى يعرض النظام إشعارًا جاريًا.
إشعار جيد يجيب عن ثلاثة أسئلة: ما الذي يتم مزامنته، ما مدى التقدم، وكيفية إيقافه. أضف إجراء إلغاء واضحًا حتى يتمكن المستخدم من التراجع إن كان على بيانات متقلبة أو يحتاج هاتفه الآن.
تقدم يمكن الوثوق به
يجب أن يعكس التقدّم وحدات حقيقية، لا نسبًا مبهمة. حدِّث التقدّم باستخدام setProgress واقرؤه من WorkInfo في واجهة المستخدم (أو شاشة الحالة).
إن كنت ترفع 12 صورة و3 نماذج، اعرض "5 من 15 عنصرًا مرفوعًا"، أرِ ما تبقّى، واحتفظ بآخر رسالة خطأ للدعم.
اجعل التقدّم ذا مغزى:
- عناصر مكتملة وعناصر متبقية\n- الخطوة الحالية ("رفع الصور"، "إرسال النماذج"، "إنهاء")\n- آخر وقت مزامنة ناجح\n- آخر خطأ (قصير، مفهوم للمستخدم)\n- خيار إلغاء/إيقاف مرئي
إن بنى فريقكم أدوات داخلية بسرعة باستخدام AppMaster، احتفظوا بنفس القاعدة: يثق المستخدمون بالمزامنة عندما يمكنهم رؤيتها، وعندما تطابق ما يحاولون فعلاً إنجازه.
العمل الفريد، الوسوم، وتجنب مهام المزامنة المكررة
المهام المكررة هي إحدى أسهل الطرق لاستنزاف البطارية، حرق بيانات الهاتف، وخلق تعارضات على الخادم. يعطيكم WorkManager أداتين بسيطتين لمنع ذلك: أسماء العمل الفريدة والوسوم.
الافتراضي الجيد هو اعتبار "المزامنة" كممر واحد. بدلًا من إدراج مهمة جديدة في كل مرة يفتح فيها التطبيق، أدرج نفس اسم العمل الفريد. بهذه الطريقة، لن تحصل على عاصفة مزامنة عندما يفتح المستخدم التطبيق، يتغير الاتصال، ويعمل جدولة دورية في نفس الوقت.
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag("sync")
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, request)
اختيار السياسة هو الاختيار السلوكي الرئيسي:
KEEP: إن كانت هناك مزامنة جارية (أو في الطابور)، تجاهل الطلب الجديد. استخدم هذا لأزرار "مزامنة الآن" ومع تشغيلات المزامنة التلقائية.\n-REPLACE: ألغِ الحالي وابدأ جديدًا. استخدم هذا عندما تغيرت المدخلات فعلاً، مثل تبديل الحساب أو اختيار مشروع مختلف.
الوسوم مفيدة للتحكم والرؤية. مع وسم ثابت مثل sync، يمكنك الإلغاء، استعلام الحالة، أو تصفية السجلات بدون تتبع IDs محددة. هذا مفيد خاصةً لإجراء "مزامنة الآن" اليدوي: يمكنك التحقق مما إذا كان هناك عمل جاري وعرض رسالة واضحة بدلًا من إطلاق عامل آخر.
لا يجب أن تتصارع المزامنة الدورية وطلبية التشغيل. احتفظ بهما منفصلين لكن منسقين:
- استخدم
enqueueUniquePeriodicWork("sync_periodic", KEEP, ...)للوظيفة المجدولة.\n- استخدمenqueueUniqueWork("sync", KEEP, ...)لطلبات عند الطلب.\n- في العامل الخاص بك، انهِ بسرعة إن لم يكن هناك ما يرفع أو ينزّل، حتى تظل التشغيلات الدورية رخيصة.\n- اختياريًا، جعل العامل الدوري يدفع نفس عمل المرة الواحدة الفريد، بحيث يحدث كل العمل الحقيقي في مكان واحد.
تحافظ هذه الأنماط على توقعية المزامنة الخلفية: مزامنة واحدة في كل مرة، سهلة الإلغاء، وسهلة الملاحظة.
خطوة بخطوة: خط أنابيب مزامنة خلفية عملي
خط أنابيب مزامنة موثوق أسهل في البناء عندما تعاملها كآلة حالات صغيرة: تعيش عناصر العمل محليًا أولًا، وWorkManager ينقلها للأمام فقط عندما تكون الظروف مناسبة.
خط أنابيب بسيط يمكن شحنه
-
ابدأ بجداول "الطابور" المحلية. خزّن أصغر بيانات وصفية تحتاجها للاستئناف: معرف العنصر، النوع (نموذج، صورة، ملاحظة)، الحالة (قيد الانتظار، جاري الرفع، تم)، عدد المحاولات، آخر خطأ، ومؤشر أو مراجعة خادم للتنزيلات.
-
لزر "مزامنة الآن" الذي يضغطه المستخدم، أدرج
OneTimeWorkRequestمع القيود التي تَطابق الواقع. اختيارات شائعة: الشبكة متصلة والبطارية ليست منخفضة. إن كانت عمليات الرفع ثقيلة، اطلب الشحن أيضًا. -
نفذ
CoroutineWorkerواحد بمراحل واضحة: رفع، تنزيل، تسوية. اجعل كل مرحلة تدريجية. ارفع فقط العناصر الموسومة pending، نزّل فقط التغييرات منذ المؤشر الأخير، ثم حافظ على قواعد تسوية بسيطة (مثلاً: الخادم يفوز لحقول التعيين، العميل يفوز للملاحظات المحلية). -
أضف إعادة محاولات مع تراجع، لكن كن انتقائيًا فيما تُعيد المحاولة عليه. المهلات و500s يجب أن تُعاد المحاولة. 401 (تسجيل خروج) يجب أن يفشل سريعًا ويخبر واجهة المستخدم بما حدث.
-
راقب
WorkInfoلقيادة واجهة المستخدم والإشعارات. استخدم تحديثات التقدم لمراحل مثل "رفع 3 من 10"، وأظهر رسالة فشل قصيرة تشير إلى الإجراء التالي (إعادة المحاولة، تسجيل الدخول، الاتصال بالواي‑فاي).
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.build()
عندما تبقي الطابور محليًا ومراحل العامل صريحة، تحصل على سلوك متوقع: يمكن للعمل التوقف، الاستئناف، وشرح نفسه للمستخدم دون تخمين ما حدث.
أخطاء شائعة وفخاخ (وكيف تتجنبها)
تفشل المزامنة الموثوقة في أغلب الأحيان بسبب بعض الاختيارات الصغيرة التي تبدو بريئة أثناء الاختبار، ثم تنهار على الأجهزة الحقيقية. الهدف ليس أن تعمل كثيرًا قدر الإمكان. هو أن تعمل في الوقت المناسب، وتؤدي العمل الصحيح، وتتوقف بشكل نظيف عندما لا تستطيع.
فخاخ يجب الحذر منها
- رفع كبير بدون قيود. إن رفعت صورًا أو حمولات كبيرة على أي شبكة وفي أي مستوى بطارية، سيشعر المستخدمون بذلك. أضف قيودًا لنوع الشبكة والبطارية المنخفضة، وقسّم العمل الكبير إلى أجزاء أصغر.
- إعادة محاولة لكل فشل إلى الأبد. 401، توكن منتهي، أو إذن مفقود ليس مشكلة مؤقتة. اعتبرها فشلًا دائمًا، واظهر إجراءً واضحًا (إعادة تسجيل الدخول)، وأعد المحاولة فقط للمشكلات العابرة مثل المهلات.
- إنشاء سجلات مكررة عن طريق الخطأ. إن كان العامل يمكن أن يعمل مرتين، سيرى الخادم إنشاء مزدوج ما لم تكن الطلبات idempotent. استخدم معرفًا ثابتًا ينشئه العميل لكل عنصر واجعل الخادم يعامل التكرارات كتحديثات، لا كسجلات جديدة.
- استخدام العمل الدوري للحاجات الزمن الحقيقي القريبة. العمل الدوري يصلح للصيانة، لا لـ"مزامنة الآن". للمزامنة التي يطلقها المستخدم، أدرج عملًا فريدًا لمرة واحدة ودعه يعمل عند الطلب.
- الإبلاغ عن "100%" مبكرًا. اكتمال الرفع ليس نفسه قبول الخادم والمصادقة. تتبع التقدم بمراحل (مُدرج، جاري الرفع، مؤكد من الخادم) وأظهر حالة مكتملة فقط بعد التأكيد.
مثال ملموس: تقني يرسل نموذجًا مع ثلاث صور في مصعد بإشارة ضعيفة. إن بدأت فورًا بدون قيود، تتعطل الرفعات، ترتفع إعادة المحاولات، وقد يُنشأ النموذج مرتين عند إعادة تشغيل التطبيق. إن قيدت إلى شبكة قابلة للاستخدام، ورفعت على خطوات، وعلّمت كل نموذج بمُعرِّف ثابت، تنتهي الحالة بسجل واحد نظيف على الخادم ورسالة تقدم صادقة.
قائمة فحص سريعة قبل الإصدار
قبل الإطلاق، اختبر المزامنة كما يكسرها مستخدمو الميدان الحقيقيون: إشارة متقطعة، بطاريات ميتة، وكثير من النقرات. ما يبدو جيدًا على هاتف مطور قد يفشل في العالم الحقيقي إذا كانت الجدولة، إعادة المحاولة، أو تقارير الحالة غير مضبوطة.
شغل هذه الاختبارات على جهاز بطيء واحد وجهاز أحدث واحد على الأقل. احتفظ بالسجلات، لكن راقب أيضًا ما يراه المستخدم في واجهة المستخدم.
- لا شبكة، ثم استعادة: ابدأ مزامنة مع إيقاف الاتصال، ثم أعده. تأكد أن العمل في الطابور (لا يفشل سريعًا)، ويستأنف لاحقًا دون تكرار الرفعات.\n- إعادة تشغيل الجهاز: ابدأ مزامنة، أعد التشغيل في منتصفها، ثم أعد فتح التطبيق. تحقق أن العمل يستمر أو يُجدول من جديد بشكل صحيح، وأن التطبيق يعرض الحالة الحالية الصحيحة (ليس متعلقًا بـ"جارٍ المزامنة").\n- بطارية منخفضة وتخزين ممتلئ: فعّل موفر البطارية، انقص البطارية تحت عتبة المنخفض إن أمكن، وملأ التخزين تقريبًا. تأكد أن المهمة تنتظر عندما يجب، ثم تستأنف عند تحسّن الظروف، دون حرق البطارية في حلقة إعادة محاولات.\n- محفزات متكررة: اضغط زر "مزامنة" عدة مرات، أو أطلق المزامنة من شاشات متعددة. يجب أن ينتهي بك الأمر بتشغيل منطقي واحد، لا كومة من العمال المتوازيين يتنافسون على نفس السجلات.\n- فشلات الخادم القابلة للتفسير: حاكي 500s، مهلات، وأخطاء مصادقة. تحقق أن إعادة المحاولات تتراجع وتتوقف بعد حد، وأن المستخدم يرى رسالة واضحة مثل "لا يمكن الوصول للخادم، سيحاول لاحقًا" بدلاً من فشل عام.
إن ترك أي اختبار التطبيق في حالة غير واضحة، اعتبر ذلك خطأ. يغفر المستخدمون المزامنة البطيئة، لكنهم لا يغفرون فقدان البيانات أو عدم معرفة ما حدث.
سيناريو مثالي: نماذج دون اتصال ورفع صور في تطبيق ميداني
يصل تقني إلى موقع بتغطية ضعيفة. يملأ نموذج خدمة دون اتصال، يلتقط 12 صورة، ويضغط إرسال قبل المغادرة. يخزن التطبيق كل شيء محليًا أولًا (مثلاً في قاعدة بيانات محلية): سجل واحد للنموذج، وسجل لكل صورة بحالة واضحة مثل PENDING, UPLOADING, DONE, أو FAILED.
عند الضغط على إرسال، يدفع التطبيق وظيفة مزامنة فريدة حتى لا تُنشأ سجلات مكررة إذا ضغط المستخدم مرتين. إعداد شائع هو سلسلة WorkManager ترفع الصور أولًا (أكبر وأبطأ)، ثم ترسل حمولة النموذج بعد تأكيد المرفقات.
تعمل المزامنة فقط عندما تطابق الظروف الواقع. على سبيل المثال، تنتظر اتصالًا متصلًا، وبطارية غير منخفضة، ومساحة تخزين كافية. إن بقي التقني في القبو بلا إشارة، لا شيء يحرق البطارية في حلقة خلفية.
التقدم واضح ومراعي للمستخدم. الرفع يعمل كعمل في المقدمة ويعرض إشعارًا مثل "رفع 3 من 12" مع زر إلغاء واضح. إن ألغى، يتوقف التطبيق وتبقى العناصر المتبقية في PENDING حتى يعيد المحاولة لاحقًا دون فقدان بيانات.
تتصرف إعادة المحاولات بأدب بعد نقطة اتصال متقلبة: المحاولة الأولى قريبًا، لكن كل فشل يزيد الانتظار (تراجع أسي). تشعر الاستجابة في البداية، ثم تتراجع لتجنّب استنزاف البطارية وإغراق الشبكة.
لفريق العمليات، العائد عملي: سجلات مكررة أقل لأن العناصر لديها مفاتيح فريدة وطابور واضح، حالات فشل مُفسّرة (أي صورة فشلت ولماذا ومتى سيعاد المحاولة)، وثقة أكبر بأن "تم الإرسال" يعني "مخزن بأمان وسيتم مزامنته".
الخطوات التالية: أطلق الموثوقية أولاً، ثم وسّع نطاق المزامنة
قبل إضافة المزيد من ميزات المزامنة، حدِّد ما يعنيه "مكتمل" بوضوح. لمعظم تطبيقات الميدان، ليس المقصود "الطلب أُرسل" بل "الخادم قبل ووافق"، زائد حالة واجهة مستخدم تطابق الواقع. يجب أن يظل النموذج الذي يقول "مزامن" كذلك بعد إعادة تشغيل التطبيق، والنموذج الفاشل يجب أن يبيّن ماذا يفعل المستخدم بعده.
اجعل التطبيق سهل الثقة بإضافة مجموعة صغيرة من الإشارات التي يمكن للمستخدمين (والدعم) رؤيتها وطرحها. اجعلها بسيطة ومتسقة عبر الشاشات:
- آخر وقت مزامنة ناجح\n- آخر خطأ في المزامنة (رسالة قصيرة، ليست تتبّع للخطأ)\n- عناصر قيد الانتظار (مثلاً: 3 نماذج، 12 صورة)\n- حالة المزامنة الحالية (خامل، مزامنة، يحتاج انتباهاً)
اعتبر القابلية للملاحظة جزءًا من الميزة. توفر ساعات في الميدان عندما يكون شخص في تغطية ضعيفة ولا يعلم إن كان التطبيق يعمل.
إن كنتم تبنون الخلفية وأدوات الإدارة أيضًا، فبناءهما معًا يساعد على إبقاء عقد المزامنة مستقرًا. AppMaster (appmaster.io) يمكنه توليد Backend جاهز للإنتاج، لوحة إدارة ويب، وتطبيقات موبايل أصلية، مما يساعد في الحفاظ على توافق النماذج والمصادقة أثناء تركيزكم على حواف المزامنة الصعبة.
أخيرًا، شغّل تجربة ميدانية صغيرة. اختر شريحة واحدة من طرف إلى طرف (مثلاً، "إرسال نموذج تفتيش مع 1-2 صورة"), وأطلقها مع القيود، إعادة المحاولات، وتقدّم مرئي للمستخدم يعملان بشكل كامل. عندما تصبح تلك الشريحة مملة ومتوقعة، وسّع ميزة واحدة في كل مرة.
الأسئلة الشائعة
يعني أن العمل الذي يُنشأ على الجهاز محفوظ محليًا أولاً وسيتم رفعه لاحقًا دون أن يضطر المستخدم لإعادة الإجراءات. يجب أن يصمد أمام إغلاق التطبيق، إعادة التشغيل، الشبكات الضعيفة، وإعادة المحاولات دون فقدان بيانات أو إنشاء سجلات مكررة.
استخدم عملًا لمرة واحدة للأحداث الحقيقية مثل "تم حفظ النموذج" أو "أضيفت صورة" أو عندما يضغط المستخدم زر Sync. استخدم العمل الدوري لصيانة النظام أو كشبكة أمان، لكنه ليس بديلاً لوظيفة الرفع المهمة لأن توقيته قد ينزلق.
إذا كنتم تستخدمون Kotlin ودوال suspend، فـCoroutineWorker هو الخيار الأبسط والأكثر توقعًا للسلوك، خاصةً لإدارة الإلغاء. استخدم Worker فقط للمهام القصيرة المحظورة، واستعمل RxWorker فقط إن كان تطبيقكم مبنيًا على RxJava بالفعل.
قم بربط الـworkers عندما تكون الخطوات لها قيود مختلفة أو يجب إعادة محاولة كل خطوة بشكل مستقل (مثل رفع ملفات كبيرة على Wi‑Fi ثم مكالمة API خفيفة على أي شبكة). استخدم عاملًا واحدًا مع مراحل واضحة عندما تشارك الخطوات نفس الحالة وتريد سلوكًا "الكل أو لا شيء" لعملية مزامنة منطقية واحدة.
اجعل كل طلب إنشاء/تعديل آمنًا للتنفيذ مرتين عن طريق استخدام مفتاح عدم التكرار (idempotency key) لكل عنصر (مثلاً UUID مخزن مع السجل المحلي). إن لم تستطع تعديل الخادم، فاحرص على upsert بمفاتيح ثابتة أو تحقق بالإصدار حتى لا تُنشأ صفوف مكررة.
احفظ حالات محليّة صريحة مثل queued، uploading، uploaded، failed حتى يتمكن العامل من الاستئناف دون تخمين. ضع علمًا بأن العنصر مُنجز فقط بعد تأكيد الخادم، وخزن البيانات الكافية (مثل URI الملف وعدد المحاولات) للمتابعة بعد تعطل أو إعادة تشغيل.
ابدأ بقيود بسيطة تحمي المستخدمين لكنها تسمح للعمل أن يحدث أغلب الأيام: تطلب اتصالًا بالشبكة، لا تعمل عند انخفاض البطارية، وتتجنب التشغيل عند امتلاء التخزين بشكل حرج. كن حذرًا مع متطلبات "غير مراقب" أو "متصل بالشاحن" لأنها قد تمنع التشغيل فعليًا على أجهزة الميدان.
تعامل مع "متصل لكن بدون إنترنت" كفشل عادي: انتهِ بسرعة، أعد المحاولة لاحقًا باستخدام Result.retry()، وإذا أمكن اكتشافه أثناء الطلب فاعرض رسالة بسيطة تشرح أن الشبكة لا تسمح بالوصول إلى الإنترنت رغم وجود اتصال Wi‑Fi.
استخدم تراجعًا أسيًا (exponential backoff) لأخطاء الشبكة حتى تقل إعادة المحاولات وتتجنب إجهاد الخادم والبطارية. أعد المحاولة على مهلات و500s وانتهِ سريعًا على مشاكل دائمة مثل طلبات غير صالحة، وحدد حدًا أعلى لعدد المحاولات حتى لا تدخل حلقة لا نهائية.
سجّل العمل كمهمة فريدة حتى لا تنشأ أعمال متواقة متعددة، وقدم تقدمًا واضحًا للمستخدم في حال الرفع الطويل. لو كانت المهمة طويلة أو مرتبطة بالمستخدم، شغّلها في المقدمة (foreground) مع إشعار دائم يُظهر الأعداد الحقيقية ويعطي خيار إلغاء واضح.


