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

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

ثبات القرص

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

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

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

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

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

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

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

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

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

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

Kotlin+KTX

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

Java

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

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

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

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

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

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

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

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 في الوقت الفعلي الأحداث "المضافة التابعة" للأحداث التي سجّل نوعين من الديناصورات من حيث النتائج، وذلك باستخدام ذاكرة التخزين المؤقت الثابتة. ولن يرفع هذا الحدث الحدث "value"، لأنّ التطبيق لم ينفّذ طلب البحث هذا على الإنترنت.

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

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

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

إدارة التواجد

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

يوفر عملاء قاعدة بيانات 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!");

طريقة عمل ميزة on Connect

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

يمكن لتطبيقك استخدام عملية الاستدعاء في عملية الكتابة لضمان إرفاق 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 في الوقت الفعلي" موقعًا خاصًا في /.info/connected يتم تعديله في كل مرة تتغيّر فيها حالة اتصال عميل "قاعدة بيانات Firebase" في الوقت الفعلي. وفي ما يلي مثال لذلك:

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 هي قيمة منطقية لا تتم مزامنتها بين عملاء قاعدة بيانات الوقت الفعلي، لأنّ القيمة تعتمد على حالة البرنامج. بعبارة أخرى، إذا قرأ أحد البرامج /.info/connected على أنّه خطأ، لا يضمن ذلك أنّ برنامجًا منفصلاً سيقرأ أيضًا القيمة "خطأ".

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

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

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

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

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 في الوقت الفعلي". يمكنك إرفاق طلب معاودة الاتصال بالموقع /.info/serverTimeOffset للحصول على القيمة بالمللي ثانية التي يضيفها عملاء قاعدة بيانات Firebase في الوقت الفعلي إلى الوقت المحلي لإعداد التقارير (الوقت الفعلي بالمللي ثانية) لتقدير وقت الخادم. يُرجى العِلم أنّ دقة هذه المعادلة يمكن أن تتأثر بوقت استجابة الشبكة، وبالتالي فهي مفيدة في المقام الأول لرصد تناقضات كبيرة (أكبر من ثانية واحدة) في وقت الساعة.

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");
    }
});

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

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

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

إليك نظام بسيط لتواجد المستخدم:

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");
    }
});