即使應用程式暫時失去網路連線,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 MB 資料。這應該足以滿足大多數應用程式的需求。如果快取超過所設定的大小,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 用戶端會使用已儲存的快取,針對分數最高的兩隻恐龍觸發「child added」事件。但由於應用程式從未在線上執行該查詢,因此不會觸發「value」事件。
如果應用程式在離線時要求最近六項項目,就會立即收到四個快取項目的「子項新增」事件。當裝置重新上線時,Firebase Realtime Database 用戶端會與伺服器同步,並取得應用程式的最後兩個「child added」和「value」事件。
處理離線交易
應用程式離線時執行的所有交易都會排入佇列。 應用程式重新連上網路後,交易就會傳送至 Realtime Database 伺服器。
管理在家狀態
在即時應用程式中,偵測用戶端連線和斷線的時間通常很有用。舉例來說,您可能會在使用者連線中斷時,將該使用者標示為「離線」。
Firebase 資料庫用戶端提供簡單的原始碼,可在用戶端與 Firebase 資料庫伺服器中斷連時,用於寫入資料庫。無論用戶端是否已順利中斷連線,這些更新都會發生,因此即使連線中斷或用戶端異常終止,您仍可依靠這些更新來清理資料。所有寫入作業 (包括設定、更新和移除) 都可以在斷線時執行。
以下是使用 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()); } } });
您也可以呼叫 .cancel()
來取消 onDisconnect
事件:
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
為 false,並不保證另一個用戶端也會讀取為 false。
在 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 用戶端在本機回報時間 (紀元時間,以毫秒為單位) 中加上的值,以便估算伺服器時間。請注意,這個偏移值的準確度可能會受到網路延遲影響,因此主要用於發現時鐘時間的差異 (超過 1 秒)。
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"); } });