تفعيل إمكانيات وضع عدم الاتصال بالإنترنت على نظام التشغيل Android

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

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

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

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

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

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

سلوك الاستمرارية

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

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

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

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

تعمل أداة Firebase Realtime Database على مزامنة وتخزين نسخة محلية من data للمستمعين النشطين. بالإضافة إلى ذلك، يمكنك مزامنة مواقع جغرافية معيّنة.

Kotlin+KTX

val scoresRef = Firebase.database.getReference("scores")
scoresRef.keepSynced(true)

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);

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

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

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

طلب البيانات بلا إنترنت

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

على سبيل المثال، يبحث هذا الرمز عن آخر أربعة عناصر في Firebase Realtime Database من النتائج.

Kotlin+KTX

val scoresRef = Firebase.database.getReference("scores")
scoresRef.orderByValue().limitToLast(4).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.orderByValue().limitToLast(4).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

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

Kotlin+KTX

scoresRef.orderByValue().limitToLast(2).addChildEventListener(object : ChildEventListener {
    override fun onChildAdded(snapshot: DataSnapshot, previousChild: String?) {
        Log.d(TAG, "The ${snapshot.key} dinosaur's score is ${snapshot.value}")
    }

    // ...
})

Java

scoresRef.orderByValue().limitToLast(2).addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(@NonNull DataSnapshot snapshot, String previousChild) {
        Log.d(TAG, "The " + snapshot.getKey() + " dinosaur's score is " + snapshot.getValue());
    }

    // ...
});

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

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

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

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

إدارة "التواجد في المنزل"

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

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

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

Kotlin+KTX

val presenceRef = Firebase.database.getReference("disconnectmessage")
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!")

Java

DatabaseReference presenceRef = FirebaseDatabase.getInstance().getReference("disconnectmessage");
// Write a string when this client loses connection
presenceRef.onDisconnect().setValue("I disconnected!");

آلية عمل onDisconnect

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

يمكن لتطبيقك استخدام دالة الاستدعاء في عملية الكتابة للتأكّد من إرفاق onDisconnect بشكل صحيح:

Kotlin+KTX

presenceRef.onDisconnect().removeValue { error, reference ->
    error?.let {
        Log.d(TAG, "could not establish onDisconnect event: ${error.message}")
    }
}

Java

presenceRef.onDisconnect().removeValue(new DatabaseReference.CompletionListener() {
    @Override
    public void onComplete(DatabaseError error, @NonNull DatabaseReference reference) {
        if (error != null) {
            Log.d(TAG, "could not establish onDisconnect event:" + error.getMessage());
        }
    }
});

يمكن أيضًا إلغاء حدث onDisconnect من خلال الاتصال بالرقم .cancel():

Kotlin+KTX

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

Java

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

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

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

Kotlin+KTX

val connectedRef = Firebase.database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue(Boolean::class.java) ?: false
        if (connected) {
            Log.d(TAG, "connected")
        } else {
            Log.d(TAG, "not connected")
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            Log.d(TAG, "connected");
        } else {
            Log.d(TAG, "not connected");
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

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

على أجهزة Android، تدير Firebase حالة الاتصال تلقائيًا بهدف تقليل معدل نقل البيانات واستخدام البطارية. عندما لا يكون لدى العميل مستمعون نشطون، أو عمليات كتابة أو onDisconnect معلّقة، ولا يتم إيقاف الاتصال صراحةً باستخدام الأسلوب goOffline، يغلق Firebase الاتصال بعد 60 ثانية من عدم النشاط.

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

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

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

Kotlin+KTX

val userLastOnlineRef = Firebase.database.getReference("users/joe/lastOnline")
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

Java

DatabaseReference userLastOnlineRef = FirebaseDatabase.getInstance().getReference("users/joe/lastOnline");
userLastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP);

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

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

Kotlin+KTX

val offsetRef = Firebase.database.getReference(".info/serverTimeOffset")
offsetRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val offset = snapshot.getValue(Double::class.java) ?: 0.0
        val estimatedServerTimeMs = System.currentTimeMillis() + offset
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled")
    }
})

Java

DatabaseReference offsetRef = FirebaseDatabase.getInstance().getReference(".info/serverTimeOffset");
offsetRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        double offset = snapshot.getValue(Double.class);
        double estimatedServerTimeMs = System.currentTimeMillis() + offset;
    }

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled");
    }
});

نموذج تطبيق "التواجد"

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

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

في ما يلي نظام بسيط لرصد تواجد المستخدمين:

Kotlin+KTX

// 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
val database = Firebase.database
val myConnectionsRef = database.getReference("users/joe/connections")

// Stores the timestamp of my last disconnect (the last time I was seen online)
val lastOnlineRef = database.getReference("/users/joe/lastOnline")

val connectedRef = database.getReference(".info/connected")
connectedRef.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(snapshot: DataSnapshot) {
        val connected = snapshot.getValue<Boolean>() ?: false
        if (connected) {
            val con = myConnectionsRef.push()

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

            // When I disconnect, update the last time I was seen online
            lastOnlineRef.onDisconnect().setValue(ServerValue.TIMESTAMP)

            // Add this device to my connections list
            // this value could contain info about the device or a timestamp too
            con.setValue(java.lang.Boolean.TRUE)
        }
    }

    override fun onCancelled(error: DatabaseError) {
        Log.w(TAG, "Listener was cancelled at .info/connected")
    }
})

Java

// 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 FirebaseDatabase database = FirebaseDatabase.getInstance();
final DatabaseReference myConnectionsRef = database.getReference("users/joe/connections");

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

final DatabaseReference connectedRef = database.getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot snapshot) {
        boolean connected = snapshot.getValue(Boolean.class);
        if (connected) {
            DatabaseReference con = myConnectionsRef.push();

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

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

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

    @Override
    public void onCancelled(@NonNull DatabaseError error) {
        Log.w(TAG, "Listener was cancelled at .info/connected");
    }
});