在 Android 裝置上啟用離線功能

即使應用程式暫時中斷網路連線,Firebase 應用程式仍可正常運作。此外,Firebase 也提供多項工具,協助您在本機保存資料、管理狀態及處理延遲情況。

磁碟持續性

Firebase 應用程式會自動處理暫時的網路中斷問題。您可在離線時存取快取資料,Firebase 會在網路連線恢復後重新傳送任何寫入資料。

啟用磁碟持續性功能後,應用程式會將資料寫入裝置本機,因此即使使用者或作業系統重新啟動應用程式,應用程式仍可在離線時維持狀態。

只要使用一行程式碼,即可啟用磁碟持續性。

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

持續性行為

啟用持續性後,即使使用者或作業系統重新啟動應用程式,Firebase 即時資料庫用戶端中的所有資料都會同步處理至磁碟,且可供離線使用。也就是說,應用程式會照常使用儲存在快取中的本機資料在線上運作。取得本機更新時,系統會持續觸發事件監聽器回呼。

Firebase 即時資料庫用戶端會自動將應用程式離線時執行的所有寫入作業排入佇列。持續性啟用後,這個佇列也會保存在磁碟中,以便在使用者或作業系統重新啟動應用程式時,取得所有寫入內容。應用程式重新連線後,所有作業都會傳送至 Firebase 即時資料庫伺服器。

如果應用程式使用 Firebase 驗證,Firebase 即時資料庫用戶端會在應用程式重新啟動時保留使用者的驗證權杖。如果驗證權杖在應用程式離線時到期,用戶端會暫停寫入作業,直到應用程式重新驗證使用者為止,否則寫入作業可能會因為安全性規則而失敗。

持續更新資料

Firebase 即時資料庫會同步處理並儲存給主動事件監聽器的資料本機副本。另外,您可以將特定地點保持同步。

Kotlin+KTX

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

Java

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

Firebase 即時資料庫用戶端會自動下載這些位置中的資料並保持同步,即使參考資料沒有啟用的事件監聽器。您可以使用以下這行程式碼關閉同步處理功能。

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

根據預設,系統會快取先前同步處理資料的 10 MB。對大多數應用程式來說應該已足夠使用。如果快取超過設定的大小,Firebase 即時資料庫會清除近期最少使用的資料。 已同步的資料不會從快取中清除。

離線查詢資料

Firebase 即時資料庫會儲存查詢傳回的資料,以便在離線時使用。針對離線時建立的查詢,Firebase 即時資料庫會繼續處理先前載入的資料。如果要求的資料尚未載入,Firebase 即時資料庫會從本機快取載入資料。等到可以重新連上網路時,系統就會載入資料並反映查詢內容。

舉例來說,這段程式碼會查詢 Firebase 即時分數資料庫中的最後四個項目

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 即時資料庫用戶端使用保留的快取,針對兩隻恐龍得到最高得分的「子項新增」事件。但應用程式從未在線上執行該查詢,因此不會觸發「值」事件。

如果應用程式在離線時要求最後六個項目,會立即收到四個快取項目的「子項新增」事件。裝置恢復連線後,Firebase 即時資料庫用戶端會與伺服器同步處理,取得最後兩個新增的「子項」和應用程式的「value」事件。

離線處理交易

凡是在應用程式離線時執行的交易,都會排入佇列。應用程式重新連上網路後,系統會將交易傳送至即時資料庫伺服器。

管理在家狀態

在即時應用程式中,偵測用戶端連線和中斷連線的時間通常很有用。舉例來說,您可以在用戶端中斷連線時,將使用者標示為「離線」。

Firebase 資料庫用戶端提供簡易的基元,當用戶端與 Firebase 資料庫伺服器中斷連線時,可用於寫入資料庫。無論用戶端是否完全中斷連線,都會進行這些更新,因此即使連線中斷或用戶端當機,您仍可仰賴更新清除資料。所有寫入作業 (包括設定、更新和移除) 都可以在連線中斷後執行。

以下是使用 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() 作業時,這項作業會在 Firebase 即時資料庫伺服器中運作。伺服器會檢查安全性,確保使用者可以執行要求的寫入事件,並在無效時通知應用程式。然後,伺服器會監控連線。無論何時,連線逾時或由即時資料庫用戶端主動關閉,伺服器會再次檢查安全性 (確認作業是否仍然有效),然後叫用事件。

應用程式可以在寫入作業中使用回呼,確保已正確附加 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());
        }
    }
});

您也可以呼叫 .cancel() 來取消 onDisconnect 事件:

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 即時資料庫會在 /.info/connected 提供特殊位置,每當 Firebase 即時資料庫用戶端的連線狀態變更時,這個位置就會更新。範例如下:

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 是布林值,因為值取決於用戶端的狀態,因此不會在即時資料庫用戶端之間同步處理。換句話說,如果一個用戶端將 /.info/connected 讀取為 false,並不保證其他用戶端也會讀取 false。

在 Android 中,Firebase 會自動管理連線狀態,以降低頻寬和電池用量。如果用戶端沒有使用中的事件監聽器、沒有待處理的寫入或 onDisconnect 作業,且未透過 goOffline 方法明確中斷連線,Firebase 會在閒置 60 秒後關閉連線。

處理延遲時間

伺服器時間戳記

Firebase 即時資料庫伺服器提供一項機制,可將伺服器產生的時間戳記插入為資料。這項功能結合 onDisconnect,可讓您輕鬆可靠地記錄即時資料庫用戶端中斷連線的時間:

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

時鐘偏差

雖然 firebase.database.ServerValue.TIMESTAMP 較為準確,且更適合大多數讀取/寫入作業,但預估用戶端與 Firebase 即時資料庫伺服器之間的時鐘偏差有時可能很實用。您可以將回呼附加至位置 /.info/serverTimeOffset,以取得 Firebase 即時資料庫用戶端加入當地回報時間 (以毫秒為單位) 來估計伺服器時間的值 (以毫秒為單位)。請注意,偏移的準確率會受到網路延遲時間的影響,因此最適合用於找出時鐘時間中的大 (超過 1 秒) 差異。

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

範例在家狀態應用程式

結合中斷連線作業、連線狀態監控和伺服器時間戳記之後,您就能建立使用者線上狀態系統。在這個系統中,每位使用者會將資料儲存於資料庫位置,藉此判斷即時資料庫用戶端是否處於連線狀態。用戶端會在連線時將這個地點設為 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");
    }
});