لا يهتم المستخدم بأن واجهتك البرمجية متوقفة أو بأن المستودع بلا تغطية. ما يهمه أن التطبيق يبدو معطّلًا. وفي العمليات الميدانية والخدمات اللوجستية والتجزئة، وفي أي مكان يكون فيه الاتصال متقطعًا، لا تُعد القدرة على العمل دون اتصال رفاهية، بل هي ما يحدد إن كان التطبيق سيُستخدم أم سيُهجر.
تضيف تطبيقات كثيرة «وضع عدم الاتصال» فكرةً لاحقة: تخزّن بضع شاشات مؤقتًا وتعرض خطأً حين يحاول المستخدم الحفظ. وهذا النهج ينهار تحديدًا في اللحظة التي تهم. أما نهج «دون اتصال أولًا» فيقلب التصميم: قاعدة البيانات المحلية على الجهاز هي المصدر الأساسي الذي تقرأ منه الواجهة، وكل كتابة تذهب إلى التخزين المحلي أولًا، والمزامنة مع الخادم عملية في الخلفية لا تنتظرها الواجهة أبدًا. فتستجيب الواجهة فورًا لأنها لا تنتظر طلب شبكة قد تنتهي مهلته، ويلحق التطبيق بالخادم حين يعود الاتصال.
ويغيّر هذا طريقة بناء الميزات؛ فكل شاشة تراقب استعلامًا محليًا، وكل إجراء يكتب محليًا ويسجّل نيّة المزامنة. وتصبح الشبكة تحسينًا لا شرطًا. وفي مشاريع Flutter لدينا نبني ذلك على أربع طبقات.
الطبقة الأولى: قاعدة بيانات محلية يمكن الوثوق بها
يحتفظ المخزن المحلي بالكيانات التي يعمل عليها المستخدم، إضافة إلى بيانات المزامنة الوصفية: معرّف ثابت يولّده العميل (مثل UUID)، وطابع زمني لآخر تعديل أو رقم إصدار، وحالة المزامنة (متزامن، أو معلّق، أو فاشل، أو متعارض).
في Flutter نوصي بخيارات تحظى بصيانة نشطة:
- drift (فوق SQLite) للبيانات العلائقية والاستعلامات محددة الأنواع والتدفقات التفاعلية وترحيل المخطط الصريح. وهو خيارنا الافتراضي حين تكون للبيانات علاقات واحتياجات تقارير.
- ObjectBox حين تريد مخزن كائنات سريعًا بواجهة بسيطة وأداء جيد مع مجموعات بيانات محلية كبيرة.
- Hive CE، الامتداد الذي يصونه المجتمع لمكتبة Hive، للتخزين الأبسط من نوع مفتاح وقيمة أو المستندات.
ونتجنب بدء مشاريع جديدة على حزمتي Hive الأصلية أو Isar؛ فكلتاهما لم تشهد صيانة تُذكر منذ عام 2023، ووُجدت النسخ المجتمعية لهذا السبب تحديدًا. وأيًّا كان اختيارك، رقّم إصدارات المخطط منذ اليوم الأول واختبر عمليات الترحيل على أجهزة حقيقية؛ فإصلاح ترحيل فاشل على هاتف في الميدان أصعب بكثير من إصلاحه على خادم.
الطبقة الثانية: طابور مزامنة بقواعد واضحة
ينتج كل تغيير محلي قيدًا في صندوق صادر (Outbox) يضم: العملية، ومعرّف الكيان، والحمولة، والوقت. ويفرّغ عامل مزامنة في الخلفية هذا الطابور عند عودة الاتصال، بالترتيب، مع إعادة المحاولة وزيادة فترة الانتظار أُسّيًا بين المحاولات.
إرشادات التصميم التي نتبعها:
- اجعل عمليات الخادم متساوية الأثر (Idempotent). أرسل المعرّف الذي يولّده العميل ومعرّفًا للعملية، كي لا يُنشئ طلبٌ أُعيدت محاولته بعد انتهاء المهلة سجلًا مكررًا.
- افصل الرفع عن الجلب. ارفع التغييرات المحلية المعلّقة، ثم اجلب تغييرات الخادم منذ آخر مزامنة ناجحة باستخدام مؤشر أو طابع زمني.
- زامن على دفعات صغيرة كي لا يعطّل سجلٌ واحد معطوب كل ما بعده، واعرض العناصر الفاشلة بدل إعادة محاولتها إلى ما لا نهاية.
- احترم قيود المنصة. التنفيذ في الخلفية على iOS وAndroid محدود ويخضع لتقدير نظام التشغيل. زامن عند فتح التطبيق أو العودة إليه، واستخدم حزمًا مثل workmanager للعمل الدوري في الخلفية حيث تسمح المنصة.
الطبقة الثالثة: قواعد تعارض تناسب المجال
تحدث التعارضات حين يتغير السجل نفسه على الجهاز وعلى الخادم قبل مزامنتهما. ولا توجد استراتيجية واحدة صحيحة، فالأمر يعتمد على البيانات:
- آخر كتابة تفوز للحقول منخفضة المخاطر مثل تفضيلات المستخدم.
- الدمج على مستوى الحقول للسجلات التي يعدّل فيها أشخاص مختلفون حقولًا مختلفة عادةً، مثل بيانات الاتصال والملاحظات.
- سجلات قابلة للإضافة فقط للأحداث مثل الزيارات والقراءات وجرد المخزون، حيث يكون كل قيد سجلًا جديدًا فتختفي التعارضات إلى حد كبير.
- المراجعة اليدوية للمعاملات المالية والموافقات وكل ما له وزن قانوني. علّم التعارض، واحتفظ بالنسختين، ودع شخصًا يقرر.
قرّر هذه القواعد مع أصحاب العمل، لا مع المطورين وحدهم؛ فهي قرارات سياسات يُعبَّر عنها بالشيفرة.
الطبقة الرابعة: واجهة متفائلة ومؤشرات صادقة
تحدّث الواجهة المتفائلة الشاشة فور الكتابة المحلية دون انتظار الخادم، وهذا ما يمنح تطبيقات «دون اتصال أولًا» إحساسًا بالسرعة. لكن يجب أن تكون صادقة: اعرض مؤشرًا صغيرًا ومتسقًا حين تكون عناصر بانتظار الرفع، واعرض وقت آخر مزامنة ناجحة، واجعل الإخفاقات ظاهرة مع طريقة واضحة لإعادة المحاولة.
يثق المستخدمون بتطبيق يصارحهم بحقيقة بياناتهم، ويهجرون تطبيقًا يفقدها بصمت.
الأمان والاختبار
قد يضيع الجهاز أو يُسرق. لذا شفّر البيانات المحلية الحساسة (مثل SQLite مع SQLCipher، أو خيارات التشفير التي توفرها قاعدة البيانات التي اخترتها)، واحفظ المفاتيح في مخزن مفاتيح المنصة عبر flutter_secure_storage، وقلّل حجم التاريخ المخزّن دون اتصال. وادعم تسجيل الخروج عن بُعد بحيث تُمسح البيانات المحلية عند التشغيل التالي.
واختبر سلوك العمل دون اتصال عن قصد: وضع الطيران، والشبكات المتذبذبة، وإغلاق التطبيق قسرًا أثناء المزامنة، وفروق الساعة بين الجهاز والخادم، وترقيات التطبيق بينما لا تزال عناصر معلّقة في الطابور.
قاعدة شيفرة واحدة وسلوك متسق
تعني قاعدة شيفرة Flutter الواحدة أن معمارية «دون اتصال أولًا» تُنفَّذ مرة واحدة وتعمل بالسلوك نفسه على Android وiOS. والطبقات الأربع أعلاه ليست عملًا إضافيًا يُلحق في النهاية، بل هي شكل التطبيق منذ شاشته الأولى. وبالنسبة للفرق التي تعمل في الميدان أو في المستودعات أو في مناطق ذات شبكات غير موثوقة، هذا ما يحوّل تطبيق الجوال إلى أداة يعتمد عليها الناس.

