انتقل للمحتوى
علي هيثم·TECH·يردّ خلال أقل من ساعة
  • الرئيسيةابدأ من هنا
  • الأعمالدراسات حالة، مشاريع
  • الخدماتما الذي سأبنيه لك
  • المدونةمقالات، سلاسل
  • التدريبدورات، مختبرات، دورات
  • عنّيالمهندس وراء هذا الموقع
تسجيل الدخولابدأ مشروع
علي هيثم · TECH
  • الرئيسية↗
  • الأعمال↗
  • الخدمات↗
  • المدونة↗
  • التدريب↗
  • عنّي↗
تسجيل الدخولابدأ مشروع
متاح
علي هيثم·TECH

استوديو هندسي. دمشق، GMT+3.

aliyosef.online

الاستوديو

  • الأعمال
  • المدونة
  • التدريب
  • عنّي
  • تواصل معي

البوابة

  • تسجيل الدخول
  • فتح تذكرة دعم
  • متابعة مشروع
  • حسابي

مصادر

  • الوثائق
  • حالة الأنظمة
  • سجلّ التغييرات
  • حزمة العلامة
  • الخصوصية
  • الشروط

النشرة

ملاحظات تقنية وأخبار، أسبوعياً. بدون حشو.

مجاني. إلغاء الاشتراك متى ما حبيت.

تابعني على
© 2026 علي هيثم يوسف. جميع الحقوق محفوظة.مبنيّ يدويًا بـ React 19.آخر نشر · 2026-05-08كل الأنظمة تعمل
  1. الرئيسية/
  2. المدوّنة/
  3. Flutter
Flutter

Flutter offline-first، الجزء الأول: الـ boot

افتتاح السلسلة. شو بيعمل التطبيق بأول 800ms لما ما في شبكة.

٦ آذار ٢٠٢٦·13 دقيقة·بقلم علي يوسف
جزء منoffline-flutter→

جاري تحميل المقال…

شاركXLinkedInRSS
استلم المقال التالي في بريدك.مقال جديد كل أحد. هندسة بلغة واضحة. مجاني، يمكنك إلغاء الاشتراك في أي وقت.اشترك→
AY
علي هيثم يوسف

مهندس وكاتب. أبني منصات متعدّدة المستأجرين، محرّكات مزامنة، وأنظمة موثوقة بهدوء. أكثر من ٦٠٠ مشروع مُسلَّم منذ ٢٠١٥.

اشتركابدأ مشروعاً→

اقرأ بعدها

  • DM
    ٢٦ شباط ٢٠٢٦·Flutter

    Drift migrations بدون فقد بيانات

    القبل، الأثناء، الـ rollback. فحوصات pre-flight أنقذتنا خمس مرات.

    9 دقائق
  • SR
    ١٨ شباط ٢٠٢٦·Flutter

    تكلفة استعادة الحالة

    إرجاع التطبيق لمكان ما تركه المستخدم. النسخة الرخيصة مقابل الغالية.

    7 دقائق
  • RB
    ١٠ شباط ٢٠٢٦·Flutter

    Riverpod مقابل Bloc، بعد سنة

    إيصالات من الإنتاج. وين بيناسب كل واحد، وين بيفشل، وشو تمنّينا حدا قاللنا.

    10 دقائق
←العودة إلى كل المقالات

يفتح المستخدم التطبيق على هاتف بدون إشارة. ماذا يحدث في أوّل ٨٠٠ مللي ثانية؟ على تطبيق «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(...) على الخطوة ٦ متعمَّد. التطبيق مُركَّب بالكامل وتفاعليّ قبل أن تحاول المزامنة البدء. إن كانت المزامنة بطيئة، المستخدم يكتب أصلاً. إن فشلت المزامنة، المستخدم يعمل أصلاً من البيانات المحلّية.

ماذا يتطلّب «التطبيق يجب أن يكون قابلاً للاستخدام» فعلاً#

ثلاثة ضمانات:

  1. كلّ استعلام قراءة لديه fallback محلّيّ. لا تعتمد أيّ شاشة على ردّ شبكة لرسم محتواها الأساسيّ. إمّا أنّ البيانات في Drift، أو الشاشة لديها تصميم empty-state.
  2. كلّ كتابة تُوضَع في queue قبل أن يرى المستخدم تأكيداً. «مُحفَظ» يعني «مُحفَظ محلّياً». إن أغلق المستخدم التطبيق وعاد، الصفّ هناك.
  3. حالة المزامنة مرئيّة. مؤشّر صغير غير ملفت (pill في الـ footer أو نقطة في الشريط العلويّ) يخبر المستخدم ما إذا كان مُزامَن، يُزامِن، أو غير متّصل. إخفاء الحالة يولّد عدم ثقة في أوّل مرّة يحدث فيها خطأ.
الكلفة الخفيّة للنقطة ١

القراءة حصراً من المحلّيّ أوّلاً يعني أنّ كلّ list view، بحث، filter، وصفحة تفاصيل يجب أن تكون مدعومة باستعلامات Drift مفهرسة. API Drift جيّد لكنّ قرارات الـ indexing قراراتك: index مركّب مفقود على contacts(tenant_id, name COLLATE) يظهر كتجمّد ٦٠ مللي ثانية حين يكتب المستخدم حرفاً في البحث. خطّط لهذا.

أين نوفّر الوقت عند الإقلاع#

تحسينان يهمّان:

تحميل كسول للجداول غير الحرجة#

Drift يفتح بسرعة، لكنّ تسجيل ٤٠ جدولاً يستغرق وقتاً متناسباً. نُقسّم الجداول إلى «حرجة عند الإقلاع» (المصادقة، الإعدادات، المستأجر الحاليّ) و «عند الطلب» (التدقيق، deleted_records، sync_queue) ونفتح المجموعة الثانية بشكل كسول عند أوّل استعلام. يقطع زمن الإقلاع بـ ~١٢٠ مللي ثانية.

الثيم من الإعدادات، لا من تحليل JSON#

نخزّن الثيم كعمود على user_prefs، لا كـ JSON blob. قراءة عمود واحد من SQLite عند الإقلاع هي ~٣ مللي ثانية؛ تحليل JSON blob والتحقّق منه هو ~٣٠ مللي ثانية. على مسار الإقلاع تلك الفجوة مرئيّة.

بدون dependency injection async#

نمرّر instance قاعدة البيانات عبر constructors، لا عبر locator على نمط get_it يجب أن ينتظر setupAsync(). كلّ غير مباشرة async عند الإقلاع تضيف microtask hop. microtask hops تتراكم.

ما التالي#

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

صفحة السلسلة تتابع البقيّة.