Flutter offline-first، الجزء الأول: الـ boot
افتتاح السلسلة. شو بيعمل التطبيق بأول 800ms لما ما في شبكة.
جزء منoffline-flutterجاري تحميل المقال…
افتتاح السلسلة. شو بيعمل التطبيق بأول 800ms لما ما في شبكة.
جزء منoffline-flutterجاري تحميل المقال…
يفتح المستخدم التطبيق على هاتف بدون إشارة. ماذا يحدث في أوّل ٨٠٠ مللي ثانية؟ على تطبيق «cloud-first»: spinner. على تطبيق offline-first، الإجابة يجب أن تكون «كلّ شيء عمل البارحة». الانتقال من الأوّل إلى الثاني يدور في معظمه حول ما تفعله عند الإقلاع، لا ما تفعله عند التشغيل.
هذا هو الجزء الأوّل من أربعة عن بناء تطبيقات Flutter تحترم الشبكات السيّئة.
تقريباً كلّ tutorial Flutter يريك runApp(MyApp()) ويمضي. في تطبيق
offline-first، ذلك السطر الواحد يُغلّف تسلسل عمليّات يجب أن تحدث
بالترتيب الصحيح وإلّا يهبط المستخدم على شاشة مكسورة. هذا التسلسل الذي
نستخدمه:
T+0ms main() يبدأ
T+10ms WidgetsFlutterBinding.ensureInitialized()
T+20ms افتح قاعدة بيانات Drift (الـ schema المحلّي)
T+50ms شغّل الترحيلات المعلّقة (شبه دائماً فارغ)
T+60ms حمّل حالة المصادقة من التخزين الآمن
T+80ms حُدّد سياق المستأجر (من حالة المصادقة)
T+100ms طبّق الثيم (من صفّ user_prefs المحلّي)
T+110ms runApp(): شجرة الواجهة تُركَّب
T+150ms أوّل إطار يُرسَم
T+200ms مزامنة الخلفيّة تحاول البدء (الشبكة اختياريّة)
T+800ms «مُزامَن للتوّ» مرئيّ إن كانت الشبكة قائمة
شيئان غير بديهيَّين. أوّلاً، كلّ شيء قبل runApp يجب أن يكون async-aware. إن حجب أيّ خطوة لأكثر من بضع مئات مللي ثانية، يرى المستخدم شاشة سوداء. ثانياً، الشبكة أبداً ليست في مسار الإقلاع الحرج. التطبيق يجب أن يكون قابلاً للاستخدام بالكامل قبل أن تُفحَص الشبكة حتّى.
ثلاث طبقات، كلّ منها بقصّة متانة مختلفة:
┌────────────────────────────────────────────────────────┐
│ Drift (SQLite) كلّ ما له شكل مجال │
│ contacts, invoices, products, sync_queue, audit_local │
├────────────────────────────────────────────────────────┤
│ Hive / SharedPrefs إعدادات KV صغيرة │
│ theme, locale, last_sync_per_entity, feature_overrides│
├────────────────────────────────────────────────────────┤
│ FlutterSecureStorage محميّ تشفيريّاً │
│ refresh_token, biometric_unlock, encryption_key │
└────────────────────────────────────────────────────────┘
لا نضع أبداً صفوف المجال في Hive. Hive سريع لكن لديه ضمانات transactional ضعيفة ولا توجد ترحيلات schema تستحقّ الاسم. حين يكون شيء «بيانات» (أيّ شيء يمكن استعلامه أو تحديثه أو ربطه)، يذهب إلى Drift. حين يكون «مقبضاً» (إعداداً، flag)، Hive جيّد.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// ١. افتح قاعدة البيانات المحلّية. هذا يجب أن ينجح؛ إن لم يفعل،
// التطبيق يعرض splash «خطأ قاعدة بيانات» ولا يصل إلى runApp.
final db = await openLocalDatabase();
// ٢. الترحيلات. Drift يتعامل مع فرق إصدار الـ schema.
// ترحيل فاشل هنا هو حدث «امسح وأعد التثبيت».
try {
await db.runMigrations();
} on MigrationFailedException catch (e) {
runApp(MigrationFailedSplash(error: e));
return;
}
// ٣. المصادقة + المستأجر. لا يُتّصل بالشبكة؛ نقرأ الحالة المخزّنة.
final authState = await SecureStorage.readAuthState();
final tenant = await db.userPrefs.currentTenant();
// ٤. الثيم. من الإعدادات المحلّية؛ افتراضيّ إلى النظام إن لم يكن مضبوطاً.
final theme = await db.userPrefs.themePreference();
// ٥. الآن لدينا كلّ ما نحتاجه لتركيب الشجرة.
runApp(AppRoot(
db: db,
initialAuth: authState,
initialTenant: tenant,
initialTheme: theme,
));
// ٦. عمل الخلفيّة. fire-and-forget؛ لا يحجب الواجهة أبداً.
unawaited(SyncService.attachAndCatchUp(db, authState));
}الـ unawaited(...) على الخطوة ٦ متعمَّد. التطبيق مُركَّب بالكامل
وتفاعليّ قبل أن تحاول المزامنة البدء. إن كانت المزامنة بطيئة، المستخدم
يكتب أصلاً. إن فشلت المزامنة، المستخدم يعمل أصلاً من البيانات
المحلّية.
ثلاثة ضمانات:
تحسينان يهمّان:
Drift يفتح بسرعة، لكنّ تسجيل ٤٠ جدولاً يستغرق وقتاً متناسباً. نُقسّم الجداول إلى «حرجة عند الإقلاع» (المصادقة، الإعدادات، المستأجر الحاليّ) و «عند الطلب» (التدقيق، deleted_records، sync_queue) ونفتح المجموعة الثانية بشكل كسول عند أوّل استعلام. يقطع زمن الإقلاع بـ ~١٢٠ مللي ثانية.
نخزّن الثيم كعمود على user_prefs، لا كـ JSON blob. قراءة عمود واحد
من SQLite عند الإقلاع هي ~٣ مللي ثانية؛ تحليل JSON blob والتحقّق منه
هو ~٣٠ مللي ثانية. على مسار الإقلاع تلك الفجوة مرئيّة.
نمرّر instance قاعدة البيانات عبر constructors، لا عبر locator على
نمط get_it يجب أن ينتظر setupAsync(). كلّ غير مباشرة async عند
الإقلاع تضيف microtask hop. microtask hops تتراكم.
الجزء الثاني من هذه السلسلة عن ترحيلات Drift نفسها: الأنماط التي تنجو من سنة من تعديلات schema بدون فقدان بيانات، الأنماط التي لا تفعل، والفحص المسبق الذي نشغّله قبل كلّ إصدار.
صفحة السلسلة تتابع البقيّة.