Włączanie funkcji offline w systemie Android

Aplikacje Firebase działają nawet wtedy, gdy aplikacja chwilowo utraci połączenie sieciowe. Ponadto Firebase udostępnia narzędzia do lokalnego utrwalania danych, zarządzania obecnością i obsługi opóźnień.

Trwałość dysku

Aplikacje Firebase automatycznie obsługują tymczasowe przerwy w sieci. Dane w pamięci podręcznej są dostępne w trybie offline, a Firebase ponownie wysyła wszelkie zapisy po przywróceniu połączenia sieciowego.

Po włączeniu trwałości dysku aplikacja zapisuje dane lokalnie na urządzeniu, dzięki czemu aplikacja może zachować stan w trybie offline, nawet jeśli użytkownik lub system operacyjny ponownie uruchomi aplikację.

Trwałość dysku można włączyć za pomocą tylko jednego wiersza kodu.

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Zachowanie trwałe

Po włączeniu trwałości wszelkie dane, które klient bazy danych Firebase Realtime Database zsynchronizowałby w trybie online, pozostają na dysku i są dostępne w trybie offline, nawet gdy użytkownik lub system operacyjny ponownie uruchomi aplikację. Oznacza to, że Twoja aplikacja działa tak samo, jak w trybie online, korzystając z danych lokalnych przechowywanych w pamięci podręcznej. Wywołania zwrotne słuchacza będą nadal uruchamiane w przypadku lokalnych aktualizacji.

Klient Firebase Realtime Database automatycznie przechowuje kolejkę wszystkich operacji zapisu wykonywanych, gdy aplikacja jest offline. Po włączeniu trwałości ta kolejka jest również utrwalana na dysku, dzięki czemu wszystkie zapisy będą dostępne, gdy użytkownik lub system operacyjny ponownie uruchomi aplikację. Gdy aplikacja odzyska połączenie, wszystkie operacje zostaną wysłane do serwera bazy danych Firebase Realtime Database.

Jeśli Twoja aplikacja korzysta z uwierzytelniania Firebase , klient bazy danych Firebase Realtime Database zachowuje token uwierzytelniania użytkownika po ponownym uruchomieniu aplikacji. Jeśli token uwierzytelniania wygaśnie, gdy aplikacja będzie w trybie offline, klient wstrzymuje operacje zapisu do czasu, aż aplikacja ponownie uwierzytelni użytkownika. W przeciwnym razie operacje zapisu mogą zakończyć się niepowodzeniem ze względu na reguły bezpieczeństwa.

Utrzymywanie świeżości danych

Baza danych Firebase Realtime synchronizuje i przechowuje lokalną kopię danych dla aktywnych słuchaczy. Ponadto możesz synchronizować określone lokalizacje.

Kotlin+KTX

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

Java

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

Klient Firebase Realtime Database automatycznie pobiera dane z tych lokalizacji i synchronizuje je, nawet jeśli odniesienie nie ma aktywnych odbiorników. Możesz ponownie wyłączyć synchronizację za pomocą następującego wiersza kodu.

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

Domyślnie buforowane jest 10 MB wcześniej zsynchronizowanych danych. To powinno wystarczyć dla większości zastosowań. Jeśli pamięć podręczna przekroczy skonfigurowany rozmiar, baza danych Firebase Realtime Database usuwa dane, które były ostatnio używane. Dane synchronizowane nie są usuwane z pamięci podręcznej.

Zapytanie o dane w trybie offline

Baza danych Firebase Realtime Database przechowuje dane zwrócone z zapytania do wykorzystania w trybie offline. W przypadku zapytań utworzonych w trybie offline baza danych Firebase Realtime Database nadal działa na wcześniej załadowanych danych. Jeśli żądane dane nie zostały załadowane, baza danych Firebase Realtime Database ładuje dane z lokalnej pamięci podręcznej. Gdy łączność sieciowa będzie ponownie dostępna, dane zostaną załadowane i będą odzwierciedlać zapytanie.

Na przykład ten kod wysyła zapytanie o cztery ostatnie elementy w bazie danych Firebase Realtime Database zawierającej wyniki

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

    // ...
});

Załóżmy, że użytkownik traci połączenie, przechodzi w tryb offline i ponownie uruchamia aplikację. Będąc nadal w trybie offline, aplikacja wysyła zapytania o dwa ostatnie elementy z tej samej lokalizacji. To zapytanie pomyślnie zwróci dwa ostatnie elementy, ponieważ aplikacja załadowała wszystkie cztery elementy w powyższym zapytaniu.

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

    // ...
});

W poprzednim przykładzie klient bazy danych Firebase Realtime Database generuje zdarzenia „dodane podrzędnie” dla dwóch dinozaurów z najwyższym wynikiem, korzystając z trwałej pamięci podręcznej. Nie wywoła to jednak zdarzenia „wartości”, ponieważ aplikacja nigdy nie wykonała tego zapytania w trybie online.

Jeśli aplikacja zażądałaby ostatnich sześciu elementów w trybie offline, natychmiast otrzymałaby zdarzenia „dodane podrzędne” dla czterech elementów znajdujących się w pamięci podręcznej. Gdy urządzenie wróci do trybu online, klient bazy danych Firebase Realtime Database synchronizuje się z serwerem i pobiera dwa ostatnie zdarzenia „dodane podrzędne” i „wartość” dla aplikacji.

Obsługa transakcji w trybie offline

Wszelkie transakcje wykonane, gdy aplikacja jest w trybie offline, są umieszczane w kolejce. Gdy aplikacja odzyska połączenie sieciowe, transakcje zostaną przesłane do serwera bazy danych czasu rzeczywistego.

Zarządzanie obecnością

W aplikacjach czasu rzeczywistego często przydatne jest wykrywanie, kiedy klienci łączą się i rozłączają. Możesz na przykład oznaczyć użytkownika jako „offline”, gdy jego klient się rozłączy.

Klienci Firebase Database udostępniają proste elementy podstawowe, których można użyć do zapisu w bazie danych, gdy klient rozłączy się z serwerami Firebase Database. Aktualizacje te pojawiają się niezależnie od tego, czy klient rozłączy się prawidłowo, czy nie, więc można na nich polegać, jeśli chodzi o czyszczenie danych nawet w przypadku zerwania połączenia lub awarii klienta. Wszystkie operacje zapisu, w tym ustawianie, aktualizowanie i usuwanie, można wykonać po rozłączeniu.

Oto prosty przykład zapisu danych po rozłączeniu przy użyciu operacji podstawowej 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!");

Jak działa onDisconnect

Po ustanowieniu operacji onDisconnect() operacja ta jest przechowywana na serwerze bazy danych Firebase Realtime Database. Serwer sprawdza bezpieczeństwo, aby upewnić się, że użytkownik może wykonać żądane zdarzenie zapisu, i informuje aplikację, jeśli jest ona nieprawidłowa. Następnie serwer monitoruje połączenie. Jeśli w dowolnym momencie połączenie przekroczy limit czasu lub zostanie aktywnie zamknięte przez klienta Realtime Database, serwer po raz drugi sprawdza bezpieczeństwo (aby upewnić się, że operacja jest nadal ważna), a następnie wywołuje zdarzenie.

Twoja aplikacja może użyć wywołania zwrotnego podczas operacji zapisu, aby upewnić się, że onDisconnect został poprawnie podłączony:

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

Zdarzenie onDisconnect można również anulować, wywołując funkcję .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();

Wykrywanie stanu połączenia

W przypadku wielu funkcji związanych z obecnością przydatne jest, aby aplikacja wiedziała, kiedy jest online, a kiedy offline. Baza danych Firebase Realtime Database udostępnia specjalną lokalizację pod /.info/connected , która jest aktualizowana za każdym razem, gdy zmienia się stan połączenia klienta Firebase Realtime Database. Oto przykład:

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 to wartość logiczna, która nie jest synchronizowana pomiędzy klientami bazy danych czasu rzeczywistego, ponieważ wartość zależy od stanu klienta. Innymi słowy, jeśli jeden klient odczyta /.info/connected jako false, nie ma gwarancji, że inny klient również odczyta false.

Na Androidzie Firebase automatycznie zarządza stanem połączenia, aby zmniejszyć przepustowość i zużycie baterii. Gdy klient nie ma aktywnych słuchaczy, nie ma żadnych oczekujących operacji zapisu ani operacji onDisconnect i nie jest jawnie rozłączany metodą goOffline , Firebase zamyka połączenie po 60 sekundach bezczynności.

Obsługa opóźnień

Znaczniki czasu serwera

Serwery Firebase Realtime Database udostępniają mechanizm wstawiania jako danych znaczników czasu wygenerowanych na serwerze. Ta funkcja w połączeniu z onDisconnect umożliwia łatwe i niezawodne zapisywanie czasu rozłączenia klienta bazy danych czasu rzeczywistego:

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

Przekrzywienie zegara

Chociaż firebase.database.ServerValue.TIMESTAMP jest znacznie dokładniejszy i preferowany w przypadku większości operacji odczytu/zapisu, czasami przydatne może być oszacowanie odchylenia zegara klienta w odniesieniu do serwerów bazy danych Firebase Realtime Database. Możesz dołączyć wywołanie zwrotne do lokalizacji /.info/serverTimeOffset , aby uzyskać wartość w milisekundach, którą klienci Firebase Realtime Database dodają do lokalnego raportowanego czasu (czas epoki w milisekundach) w celu oszacowania czasu serwera. Należy pamiętać, że na dokładność tego przesunięcia może wpływać opóźnienie sieci, dlatego jest ono przydatne przede wszystkim do wykrywania dużych (> 1 sekundy) rozbieżności w czasie zegara.

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

Przykładowa aplikacja obecności

Łącząc operacje rozłączania z monitorowaniem stanu połączenia i znacznikami czasu serwera, możesz zbudować system obecności użytkowników. W tym systemie każdy użytkownik przechowuje dane w lokalizacji bazy danych, aby wskazać, czy klient bazy danych czasu rzeczywistego jest online. Klienci ustawiają tę lokalizację na wartość true, gdy łączą się z Internetem, oraz sygnaturę czasową, gdy się rozłączają. Ten znacznik czasu wskazuje, kiedy dany użytkownik był online po raz ostatni.

Pamiętaj, że aplikacja powinna kolejkować operacje rozłączenia, zanim użytkownik zostanie oznaczony w trybie online, aby uniknąć sytuacji wyścigowych w przypadku utraty połączenia sieciowego klienta przed wysłaniem obu poleceń na serwer.

Oto prosty system obecności użytkownika:

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