הפעלת יכולות במצב אופליין ב-Android

אפליקציות Firebase פועלות גם אם האפליקציה מאבדת באופן זמני את החיבור לרשת. בנוסף, Firebase מספק כלים לשמירת נתונים באופן מקומי, לניהול נוכחות ולטיפול בזמן אחזור.

אחסון מתמיד (persistent disk)

אפליקציות Firebase מטפלות באופן אוטומטי בהפרעות זמניות ברשת. הנתונים שנשמרו במטמון זמינים במצב אופליין, ו-Firebase שולח מחדש את כל הנתונים שהוגדרו ככתיבה כשהחיבור לרשת מתחדש.

כשמפעילים את התכונה 'עמידות בדיסק', האפליקציה כותבת את הנתונים באופן מקומי במכשיר כדי שהיא תוכל לשמור את המצב שלה במצב אופליין, גם אם המשתמש או מערכת ההפעלה מפעילים מחדש את האפליקציה.

אפשר להפעיל את העמידות בדיסק באמצעות שורת קוד אחת בלבד.

Kotlin+KTX

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+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);

כברירת מחדל, 10MB של נתונים שסונכרנו בעבר נשמרים במטמון. זה אמור להספיק לרוב האפליקציות. אם המטמון חורג מהגודל שהוגדר לו, המערכת מבצעת 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 יוצר אירועים מסוג 'child added' לשני הדינוזאורים עם הדירוג הגבוה ביותר, באמצעות המטמון הקבוע. אבל לא יופעל אירוע 'value', כי האפליקציה אף פעם לא הפעילה את השאילתה הזו בזמן שהיא הייתה מחוברת לאינטרנט.

אם האפליקציה תבקש את ששת הפריטים האחרונים במצב אופליין, היא תקבל אירועים מסוג 'הוספת צאצא' עבור ארבעת הפריטים שנשמרו במטמון באופן מיידי. כשהמכשיר חוזר לאינטרנט, הלקוח Firebase Realtime Database מסנכרן עם השרת ומקבל את שני האירועים האחרונים מסוג 'child added' ואת האירוע 'value' לאפליקציה.

טיפול בעסקאות אופליין

כל העסקאות שמבוצעות כשהאפליקציה במצב אופליין נכנסות לתור. כשהאפליקציה מתחברת מחדש לרשת, העסקאות נשלחות לשרת Realtime Database.

ניהול הנוכחות

באפליקציות בזמן אמת, לרוב כדאי לזהות מתי לקוחות מתחברים ומתי הם מתנתקים. לדוגמה, יכול להיות שתרצו לסמן משתמש כ 'לא מקוון' כשהלקוח שלו מתנתק.

לקוחות של Firebase Database מספקים פרימיטיבים פשוטים שאפשר להשתמש בהם כדי לכתוב במסד הנתונים כשלקוח מתנתק מהשרתים של Firebase Database. העדכונים האלה מתרחשים גם אם הלקוח מתנתק בצורה תקינה וגם אם לא, כך שאפשר להסתמך עליהם כדי לנקות נתונים גם אם החיבור התנתק או שהלקוח קרס. אפשר לבצע את כל פעולות הכתיבה, כולל הגדרה, עדכון והסרה, גם כשהמכשיר לא מחובר.

דוגמה פשוטה לכתיבה של נתונים לאחר ניתוק באמצעות הפרימיטיב 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, השרת בודק את האבטחה בפעם השנייה (כדי לוודא שהפעולה עדיין תקפה) ולאחר מכן מפעיל את האירוע.

האפליקציה יכולה להשתמש בקריאה החוזרת (callback) של פעולת הכתיבה כדי לוודא שה-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 כ-false, אין ערובה לכך שלקוח נפרד יקרא גם הוא את הערך כ-false.

ב-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);

Clock Skew

הערך של 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 מחובר או לא. לקוחות מגדירים את המיקום הזה כ-true כשהם מחוברים לאינטרנט, ומגדירים חותמת זמן כשהם מתנתקים. חותמת הזמן הזו מציינת את הפעם האחרונה שהמשתמש היה אונליין.

חשוב לזכור שהאפליקציה צריכה להוסיף את פעולות הניתוק לתור לפני שמשתמש מסומן אופליין, כדי למנוע תנאי מרוץ במקרה שחיבורי הרשת של הלקוח יתנתקו לפני שאפשר יהיה לשלוח את שתי הפקודות לשרת.

הנה מערכת פשוטה לזיהוי נוכחות משתמשים:

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