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

في ما يلي مثال بسيط على كتابة البيانات عند قطع الاتصال باستخدام العنصر الأساسي 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 لأنّ القيمة تعتمد على حالة العميل. بعبارة أخرى، إذا قرأ أحد العملاء القيمة /.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);

Clock Skew

مع أنّ 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");
    }
});

تطبيق Sample Presence

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

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

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

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