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

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

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

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

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

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

Kotlin

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

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

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

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

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

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

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

Kotlin

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

Java

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

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

Kotlin

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

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

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

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

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

Kotlin

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

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

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

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

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

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

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

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

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

Kotlin

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

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

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

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

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

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

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

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

Kotlin

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

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

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

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

Kotlin

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