אפליקציות 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"); } });