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

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

aliyosef.online

الاستوديو

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

البوابة

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

مصادر

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

النشرة

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

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

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

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

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

١٨ شباط ٢٠٢٦·7 دقائق·بقلم علي يوسف

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

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

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

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

اقرأ بعدها

  • OF
    Series
    ٦ آذار ٢٠٢٦·Flutter

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

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

    13 دقيقة
  • DM
    ٢٦ شباط ٢٠٢٦·Flutter

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

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

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

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

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

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

مستخدم يملأ فاتورة. نظام التشغيل يقتل التطبيق لتحرير ذاكرة. يعود المستخدم. يتوقّع أن يعود إلى شاشة الفاتورة، مع القيم المكتوبة لا تزال هناك. الطريقة الرخيصة لتقديم هذا تنكسر تحت الحمل. الطريقة الباهظة تهدر البطّاريّة. شحنّا الاثنتَين وانتهينا في مكان ما في الوسط.

هذا المقال عمّا تكلّفه استعادة الحالة، والمقايضات التي قرّرت أين هبطنا.

ماذا تغطّي «استعادة الحالة» فعلاً#

ثلاثة أشياء مختلفة يجمعها الناس تحت نفس الاسم:

  1. حالة التنقّل. أيّ مسار المستخدم عليه. سهل التعامل: تسلسل المسار الحاليّ + المعاملات في كلّ تنقّل.
  2. بيانات النموذج قيد التقدّم. المستخدم كتب رقم هاتف وعنواناً؛ إن فقدنا ذلك، لن يكتبه مجدّداً. أصعب.
  3. مواضع التمرير ومُكِّنات الواجهة. القائمة كانت ممرّرة إلى الصفّ ٢٤٧؛ ورقة كانت نصف مفتوحة؛ filter كان مطبّقاً. الأرخص للتخطّي والأكثر افتقاداً حين تفعل.

كلّ منها لديه ملفّ كلفة مختلف. نحسّنها بشكل منفصل.

النسخة الرخيصة#

النسخة التي تشحنها معظم التطبيقات هي «احفظ كلّ شيء على القرص في كلّ ضغطة مفتاح». تحديداً، في Flutter:

class InvoiceFormController extends ValueNotifier<InvoiceFormState> {
  InvoiceFormController() : super(InvoiceFormState.empty()) {
    addListener(_persist);
  }

  Future<void> _persist() async {
    await Hive.box('drafts').put('current_invoice', value.toJson());
  }
}

كلّ تغيير في النموذج يطلق كتابة إلى Hive. Hive سريع، فـ «سريع» يبدو جيّداً في التطوير. في الإنتاج:

  • مستخدم يكتب رقم هاتف من ١٢ حرفاً يطلق ١٢ كتابة.
  • على Android متدنّ، كلّ كتابة تضرب تخزين الفلاش وتستهلك بطّاريّة. ملفّ تعريف CPU التطبيق يُظهر نشاطاً منخفض المستوى مستمرّاً كلّما كان نموذج مفتوحاً.
  • القرص يبلى أسرع ممّا يجب. معظم المستخدمين لا يلاحظون أبداً؛ بعضهم يفعل.

الإصلاح هو throttling الكتابات. الخطأ هو افتراض أنّ throttling يحلّ المشكلة.

ماذا لا يحلّ throttling وحده#

«احفظ كلّ ٢٥٠ مللي ثانية» مع debounce يقلّص حجم الكتابة بـ ١٠-٢٠×. لا يغيّر الصورة لسببَين:

  1. الكتابة لا تزال تحدث على timer. يمكن للمستخدم ألّا يفعل شيئاً ولا نزال نكتب نفس الحالة التي كتبناها قبل ٢٥٠ مللي ثانية.
  2. الكتابة تحوي النموذج كلّه. فاتورة من ٥٠ سطر بـ ٨ بنود تتسلسل إلى بضعة KB؛ نحن نكتب تلك الـ KB إلى القرص حتّى حين يتغيّر اسم العميل فقط.

احتجنا أن نجعل الكتابات أنذر وأصغر.

الشكل الذي نشحنه الآن#

ثلاثة تغييرات:

١. احفظ على أحداث lifecycle، لا على كلّ ضغطة مفتاح#

WidgetsBindingObserver في Flutter يكشف أحداث lifecycle. نوصل controller النموذج بها ونكتب فقط حين:

  • يذهب التطبيق إلى الخلفيّة (AppLifecycleState.paused).
  • يتنقّل المستخدم بعيداً عن النموذج (pop / replace المسار).
  • يُضغَط زرّ «احفظ المسوّدة» صريحاً.
class InvoiceFormController extends ChangeNotifier with WidgetsBindingObserver {
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _persistDraft(); // أطلق وانسَ
    }
  }
}

هذا التغيير الواحد قطع معدّل كتابتنا للفلاش بـ ~٩٥٪. المستخدم يكتب بحرّيّة؛ النموذج يُحفَظ حين يغادر أو يذهب التطبيق إلى الخلفيّة.

٢. احفظ diffs، لا النموذج كلّه#

لبيانات طويلة الشكل (فاتورة مع بنود)، نحتفظ بـ snapshot lastSaved ونحفظ JSON-patch بين الحاليّ والأخير فقط. إعادة التحميل تُعيد البناء بتطبيق الـ patch على الـ snapshot.

final patch = json_patch.diff(state.lastSaved, state.toJson());
if (patch.isNotEmpty) {
  await Hive.box('drafts').put('invoice_${formId}', {
    'snapshot_at': state.lastSavedAt,
    'patch': patch,
  });
}

متوسّط حجم الحمولة انخفض من ٤ KB إلى ٢٠٠ بايت. على جهاز متدنّ الفرق غير قابل للقياس؛ في المجموع عبر آلاف المستخدمين على فلاش بطيء يتراكم.

٣. أسقط مواضع التمرير على جدول مختلف#

موضع التمرير بيانات عالية التردّد، منخفضة الأهمّية. حفظها على كلّ حدث تمرير عديم الجدوى؛ حفظها على أحداث lifecycle متأخّر. نتنازل: debounce ١ ثانية بينما المستخدم يمرّر بنشاط، يُحفَظ في الذاكرة فقط، ويُفرَّغ إلى القرص على أحداث lifecycle مع كلّ شيء آخر.

استعادة التمرير: عند استئناف التطبيق، ننتظر أوّل إطار، ثمّ نقفز إلى الـ offset المحفوظ. إن كانت القائمة قد أُعيدت جلبها ولم يعد الـ offset ذا معنى (نادر)، نثبّت إلى أقرب رأس لاصق معروف.

ما لا نستعيده عمداً

أوراق سفليّة كانت نصف مفتوحة. محدّدات modal. حالة لوحة المفاتيح. أيّ واجهة «عابرة» كانت مرفوعة لحظة الـ pause تُلغى عند الاستعادة. استعادتها تُدخل حالات حدّيّة غير متناسبة مع قيمة المستخدم: المستخدم يعرف ما كان يفعل، لا يحتاج أن يعود snapshot الـ OS-killed تماماً. التنقّل + بيانات النموذج + التمرير كافٍ.

قياس ما إذا كان يعمل#

مقياسان:

  1. معدّل فقدان الاستعادة. نسبة دورات pause-resume التي فُقدت فيها بيانات المستخدم المكتوبة. نسبتنا < ٠٫١٪. الفاقد هو انهيارات التطبيق بين الحفظ وتفريغ القرص، نقبلها.
  2. عبء البطّاريّة من المسوّدات. trace منفصل يتتبّع البايتات المكتوبة لكلّ جلسة. نهدف إلى أقلّ من ٨ KB لجلسة نموذج نموذجيّة من ٥ دقائق. نضرب حالياً ~٣ KB.

كلا الرقمَين على لوحة معلومات. أوّل مرّة شحنّا فيها تغيير diff-persist، انخفض رقم البايتات المكتوبة ٢٠×، لكنّنا لم نلاحظ لأسبوعَين لأنّ لا أحد كان ينظر. الآن نفعل.

المبدأ#

استعادة الحالة ليست «احفظ كلّ شيء»؛ هي «احفظ الأشياء التي يهتمّ المستخدم بفقدها، في اللحظات التي يكون فيها الحفظ رخيصاً». بيانات النموذج التي كتبها: نعم. البكسل الفرعيّ الدقيق لموضع تمريره: لا. معظم الكلفة في النسخة الرخيصة تأتي من حفظ الأشياء الخاطئة.

الجزء التالي من هذه السلسلة عن Riverpod مقابل Bloc، مع سنة من إيصالات الإنتاج. صفحة السلسلة تتابع البقيّة.