تفعيل إمكانيات الاتصال بلا إنترنت

تعمل تطبيقات Firebase حتى إذا فقد تطبيقك مؤقتًا اتصاله بالشبكة. بالإضافة إلى ذلك، توفّر Firebase أدوات للاحتفاظ بالبيانات محليًا وإدارة حالة الظهور والتعامل مع وقت الاستجابة.

الاحتفاظ بالبيانات على القرص

تتعامل تطبيقات Firebase تلقائيًا مع الانقطاعات المؤقتة في الشبكة. تتوفّر البيانات المخزّنة مؤقتًا أثناء عدم الاتصال بالإنترنت، وتعيد Firebase إرسال أي عمليات كتابة عند استعادة الاتصال بالشبكة.

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

يمكنك تفعيل ميزة الاحتفاظ بالبيانات على القرص باستخدام سطر واحد فقط من التعليمات البرمجية.

FirebaseDatabase.instance.setPersistenceEnabled(true);

سلوك الاحتفاظ بالبيانات

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

يحتفظ عميل قاعدة بيانات Firebase في الوقت الفعلي تلقائيًا بقائمة انتظار لجميع عمليات الكتابة التي يتم إجراؤها أثناء عدم اتصال تطبيقك بالإنترنت. عند تفعيل ميزة الاحتفاظ بالبيانات، يتم أيضًا الاحتفاظ بقائمة الانتظار هذه على القرص، ما يتيح لك الوصول إلى جميع عمليات الكتابة عند إعادة تشغيل التطبيق من قِبل المستخدم أو نظام التشغيل. وعند استعادة الاتصال بالتطبيق، يتم إرسال جميع العمليات إلى خادم قاعدة بيانات Firebase في الوقت الفعلي.

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

الحفاظ على حداثة البيانات

تُزامن قاعدة بيانات Firebase في الوقت الفعلي نسخة محلية من البيانات وتخزّنها للمستمعين النشطين. بالإضافة إلى ذلك، يمكنك الحفاظ على مزامنة مواقع معيّنة.

final scoresRef = FirebaseDatabase.instance.ref("scores");
scoresRef.keepSynced(true);

ينزّل عميل قاعدة بيانات Firebase في الوقت الفعلي تلقائيًا البيانات في هذه المواقع ويحافظ على مزامنتها حتى إذا لم يكن للمرجع أي مستمعين نشطين. يمكنك إيقاف المزامنة مرة أخرى باستخدام سطر التعليمات البرمجية التالي.

scoresRef.keepSynced(false);

يتم تلقائيًا تخزين 10 ميغابايت من البيانات التي تمت مزامنتها سابقًا في ذاكرة التخزين المؤقت. ويجب أن يكون هذا كافيًا لمعظم التطبيقات. إذا تجاوزت ذاكرة التخزين المؤقت الحجم الذي تم ضبطه، تحذف قاعدة بيانات Firebase في الوقت الفعلي البيانات التي تم استخدامها الأقل مؤخرًا. لا يتم حذف البيانات التي يتم الحفاظ على مزامنتها من ذاكرة التخزين المؤقت.

الاستعلام عن البيانات بلا إنترنت

تخزّن قاعدة بيانات Firebase في الوقت الفعلي البيانات التي يتم عرضها من طلب بحث لاستخدامها عند عدم الاتصال بالإنترنت. بالنسبة إلى طلبات البحث التي يتم إنشاؤها أثناء عدم الاتصال بالإنترنت، تواصل قاعدة بيانات Firebase في الوقت الفعلي العمل مع البيانات التي تم تحميلها سابقًا. إذا لم يتم تحميل البيانات المطلوبة، تحمّل قاعدة بيانات Firebase في الوقت الفعلي البيانات من ذاكرة التخزين المؤقت المحلية. عندما يتوفّر الاتصال بالشبكة مرة أخرى، يتم تحميل البيانات وتعرض طلب البحث.

على سبيل المثال، يستعلم هذا الرمز عن آخر أربعة عناصر في قاعدة بيانات للنتائج:

final scoresRef = FirebaseDatabase.instance.ref("scores");
scoresRef.orderByValue().limitToLast(4).onChildAdded.listen((event) {
  debugPrint("The ${event.snapshot.key} dinosaur's score is ${event.snapshot.value}.");
});

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

scoresRef.orderByValue().limitToLast(2).onChildAdded.listen((event) {
  debugPrint("The ${event.snapshot.key} dinosaur's score is ${event.snapshot.value}.");
});

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

إذا طلب التطبيق آخر ستة عناصر أثناء عدم الاتصال بالإنترنت، سيحصل على أحداث "تمت إضافة عنصر فرعي" للعناصر الأربعة المخزّنة مؤقتًا على الفور. عندما يعود الجهاز إلى الإنترنت، يُزامن عميل قاعدة بيانات Firebase في الوقت الفعلي البيانات مع الخادم ويحصل على آخر حدثَين من نوع "تمت إضافة عنصر فرعي" وحدث "القيمة" للتطبيق.

التعامل مع المعاملات بلا إنترنت

تتم إضافة أي معاملات يتم إجراؤها أثناء عدم اتصال التطبيق بالإنترنت إلى قائمة الانتظار. وبمجرد استعادة اتصال التطبيق بالشبكة، يتم إرسال المعاملات إلى خادم قاعدة البيانات في الوقت الفعلي.

تتضمّن قاعدة بيانات Firebase في الوقت الفعلي العديد من الميزات للتعامل مع سيناريوهات عدم الاتصال بالإنترنت والاتصال بالشبكة. ينطبق باقي هذا الدليل على تطبيقك سواء فعّلت ميزة الاحتفاظ بالبيانات أم لا.

إدارة حالة الظهور

في التطبيقات التي تعمل في الوقت الفعلي، من المفيد غالبًا رصد حالات اتصال العملاء وانقطاعهم. على سبيل المثال، قد تريد وضع علامة "غير متصل بالإنترنت" على مستخدم عند انقطاع اتصال عميله.

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

في ما يلي مثال بسيط على كتابة البيانات عند انقطاع الاتصال باستخدام العنصر الأساسي onDisconnect:

final presenceRef = FirebaseDatabase.instance.ref("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().set("I disconnected!");

آلية عمل onDisconnect

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

try {
    await presenceRef.onDisconnect().remove();
} catch (error) {
    debugPrint("Could not establish onDisconnect event: $error");
}

يمكن أيضًا إلغاء حدث onDisconnect من خلال استدعاء .cancel():

final onDisconnectRef = presenceRef.onDisconnect();
onDisconnectRef.set("I disconnected");
// ...
// some time later when we change our minds
// ...
onDisconnectRef.cancel();

رصد حالة الاتصال

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

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    debugPrint("Connected.");
  } else {
    debugPrint("Not connected.");
  }
});

/.info/connected هي قيمة منطقية لا تتم مزامنتها بين عملاء قاعدة بيانات الوقت الفعلي لأنّ القيمة تعتمد على حالة العميل. بعبارة أخرى، إذا قرأ أحد العملاء /.info/connected على أنّه false، لا يعني ذلك بالضرورة أنّ عميلاً منفصلاً سيقرأ أيضًا false.

التعامل مع وقت الاستجابة

الطوابع الزمنية للخادم

توفّر خوادم قاعدة بيانات Firebase في الوقت الفعلي آلية لإدراج الطوابع الزمنية التي يتم إنشاؤها على الخادم كبيانات. توفّر هذه الميزة، بالإضافة إلى onDisconnect، طريقة سهلة لتسجيل الوقت الذي انقطع فيه اتصال عميل قاعدة بيانات في الوقت الفعلي بشكلٍ موثوق:

final userLastOnlineRef =
    FirebaseDatabase.instance.ref("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().set(ServerValue.timestamp);

انحراف الساعة

على الرغم من أنّ ServerValue.timestamp أكثر دقة بكثير، ويُفضّل استخدامه لمعظم عمليات القراءة والكتابة، قد يكون من المفيد أحيانًا تقدير انحراف ساعة العميل بالنسبة إلى خوادم قاعدة بيانات Firebase في الوقت الفعلي. يمكنك إرفاق معاودة اتصال بالموقع /.info/serverTimeOffset للحصول على القيمة بالملّي ثانية التي يضيفها عملاء قاعدة بيانات Firebase في الوقت الفعلي إلى الوقت المحلي الذي يتم الإبلاغ عنه (وقت الحقبة بالملّي ثانية) لتقدير وقت الخادم. يُرجى العِلم أنّ دقة هذا الإزاحة يمكن أن تتأثر بوقت استجابة الشبكة، لذا من المفيد بشكلٍ أساسي رصد الاختلافات الكبيرة (> ثانية واحدة) في وقت الساعة.

final offsetRef = FirebaseDatabase.instance.ref(".info/serverTimeOffset");
offsetRef.onValue.listen((event) {
  final offset = event.snapshot.value as num? ?? 0.0;
  final estimatedServerTimeMs =
      DateTime.now().millisecondsSinceEpoch + offset;
});

نموذج تطبيق حالة الظهور

من خلال الجمع بين عمليات انقطاع الاتصال ورصد حالة الاتصال والطوابع الزمنية للخادم، يمكنك إنشاء نظام لحالة ظهور المستخدم. في هذا النظام، يخزّن كل مستخدم بيانات في موقع قاعدة بيانات للإشارة إلى ما إذا كان عميل قاعدة بيانات الوقت الفعلي متصلاً بالإنترنت أم لا. يضبط العملاء هذا الموقع على "صحيح" عند الاتصال بالإنترنت ويضبطون طابعًا زمنيًا عند انقطاع الاتصال. يشير هذا الطابع الزمني إلى آخر مرة كان فيها المستخدم المحدّد متصلاً بالإنترنت.

يُرجى العِلم أنّه يجب أن يضيف تطبيقك عمليات انقطاع الاتصال إلى قائمة الانتظار قبل وضع علامة على أنّ المستخدم متصل بالإنترنت، وذلك لتجنُّب أي حالات تعارض في حال فقدان اتصال العميل بالشبكة قبل إرسال كلا الأمرَين إلى الخادم.

// Since I can connect from multiple devices, we store each connection
// instance separately any time that connectionsRef's value is null (i.e.
// has no children) I am offline.
final myConnectionsRef =
    FirebaseDatabase.instance.ref("users/joe/connections");

// Stores the timestamp of my last disconnect (the last time I was seen online)
final lastOnlineRef =
    FirebaseDatabase.instance.ref("/users/joe/lastOnline");

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    final con = myConnectionsRef.push();

    // When this device disconnects, remove it.
    con.onDisconnect().remove();

    // When I disconnect, update the last time I was seen online.
    lastOnlineRef.onDisconnect().set(ServerValue.timestamp);

    // Add this device to my connections list.
    // This value could contain info about the device or a timestamp too.
    con.set(true);
  }
});