Włączanie funkcji offline na urządzeniach z Androidem

Aplikacje Firebase działają nawet wtedy, gdy aplikacja tymczasowo utraci połączenie z siecią. Dodatkowo Firebase udostępnia narzędzia do trwałego przechowywania danych lokalnie, zarządzania obecnością i obsługi opóźnień.

Trwałość dysku

Aplikacje Firebase automatycznie obsługują tymczasowe przerwy w działaniu sieci. Dane w pamięci podręcznej są dostępne w trybie offline, a Firebase ponownie wysyła wszystkie zapisy, gdy połączenie sieciowe zostanie przywrócone.

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

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

Kotlin

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Zachowanie trwałości

Włączenie trwałości powoduje, że wszystkie dane, które klient Firebase Realtime Database synchronizuje w trybie online, są zapisywane na dysku i dostępne w trybie offline, nawet gdy użytkownik lub system operacyjny ponownie uruchomi aplikację. Oznacza to, że aplikacja działa tak samo jak w trybie online, korzystając z lokalnych danych przechowywanych w pamięci podręcznej. Wywołania zwrotne odbiornika będą nadal wywoływane w przypadku lokalnych aktualizacji.

Klient Firebase Realtime Database automatycznie utrzymuje kolejkę wszystkich operacji zapisu wykonywanych, gdy aplikacja jest offline. Gdy trwałość jest włączona, ta kolejka jest też zapisywana na dysku, dzięki czemu wszystkie zapisy są dostępne, gdy użytkownik lub system operacyjny ponownie uruchomi aplikację. Gdy aplikacja odzyska połączenie, wszystkie operacje zostaną wysłane na serwer Firebase Realtime Database.

Jeśli Twoja aplikacja korzysta z Uwierzytelniania Firebase, klient Firebase Realtime Database przechowuje token uwierzytelniania użytkownika między ponownymi uruchomieniami aplikacji. Jeśli token uwierzytelniania wygaśnie, gdy aplikacja jest offline, klient wstrzyma operacje zapisu do czasu ponownego uwierzytelnienia użytkownika przez aplikację. W przeciwnym razie operacje zapisu mogą się nie powieść z powodu reguł zabezpieczeń.

Odświeżanie danych

Firebase Realtime Database synchronizuje i przechowuje lokalną kopię danych dla aktywnych słuchaczy. Możesz też synchronizować konkretne lokalizacje.

Kotlin

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 odwołanie nie ma aktywnych odbiorców. Synchronizację możesz ponownie wyłączyć za pomocą tego wiersza kodu:

Kotlin

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

Domyślnie w pamięci podręcznej przechowywane jest 10 MB wcześniej zsynchronizowanych danych. Powinno to wystarczyć w przypadku większości aplikacji. Jeśli pamięć podręczna przekroczy skonfigurowany rozmiar, Firebase Realtime Database usunie dane, które były używane najrzadziej. Dane, które są synchronizowane, nie są usuwane z pamięci podręcznej.

Tworzenie zapytań o dane offline

Firebase Realtime Database przechowuje dane zwrócone przez zapytanie do użytku w trybie offline. W przypadku zapytań utworzonych w trybie offline funkcja Firebase Realtime Database nadal działa w odniesieniu do wcześniej wczytanych danych. Jeśli żądane dane nie zostały wczytane, Firebase Realtime Database wczytuje dane z lokalnej pamięci podręcznej. Gdy połączenie sieciowe będzie ponownie dostępne, dane zostaną załadowane i będą odzwierciedlać zapytanie.

Na przykład ten kod wysyła zapytanie o 4 ostatnie elementy w Firebase Realtime Database wyników.

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

    // ...
});

Załóżmy, że użytkownik traci połączenie, przechodzi w tryb offline i ponownie uruchamia aplikację. W trybie offline aplikacja wysyła zapytanie o 2 ostatnie elementy z tej samej lokalizacji. To zapytanie zwróci 2 ostatnie elementy, ponieważ aplikacja wczytała wszystkie 4 elementy w zapytaniu powyżej.

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

    // ...
});

W powyższym przykładzie klient Firebase Realtime Database zgłasza zdarzenia „dodano element podrzędny” w przypadku 2 dinozaurów z najwyższymi wynikami, korzystając z utrwalonego bufora. Nie wywoła jednak zdarzenia „value”, ponieważ aplikacja nigdy nie wykonała tego zapytania w trybie online.

Jeśli aplikacja poprosi o 6 ostatnich elementów w trybie offline, od razu otrzyma zdarzenia „dodano dziecko” dla 4 elementów w pamięci podręcznej. Gdy urządzenie wró do trybu online, klient Firebase Realtime Database zsynchronizuje się z serwerem i otrzyma 2 ostateczne zdarzenia „child added” oraz zdarzenie „value” dla aplikacji.

Obsługa transakcji offline

Wszystkie transakcje przeprowadzane w trybie offline są umieszczane w kolejce. Gdy aplikacja ponownie nawiąże połączenie z siecią, transakcje zostaną wysłane na serwer Realtime Database.

Zarządzanie obecnością

W aplikacjach działających w czasie rzeczywistym często przydaje się wykrywanie, kiedy klienci się łączą i rozłączają. Możesz na przykład oznaczyć użytkownika jako „offline”, gdy jego klient się rozłączy.

Klienci Bazy danych Firebase udostępniają proste elementy, których możesz używać do zapisywania danych w bazie, gdy klient odłączy się od serwerów Bazy danych Firebase. Te aktualizacje są wykonywane niezależnie od tego, czy klient rozłączy się prawidłowo, czy nie. Możesz więc na nich polegać przy czyszczeniu danych, nawet jeśli połączenie zostanie przerwane lub klient ulegnie awarii. Wszystkie operacje zapisu, w tym ustawianie, aktualizowanie i usuwanie, można wykonywać po odłączeniu.

Oto prosty przykład zapisywania danych po odłączeniu za pomocą elementu 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!");

Jak działa funkcja onDisconnect

Gdy utworzysz operację onDisconnect(), będzie ona przechowywana na serwerze Firebase Realtime Database. Serwer sprawdza zabezpieczenia, aby upewnić się, że użytkownik może wykonać żądane zdarzenie zapisu, i informuje aplikację, czy jest ono nieprawidłowe. Serwer monitoruje połączenie. Jeśli w dowolnym momencie połączenie zostanie przerwane lub aktywnie zamknięte przez klienta Realtime Database, serwer po raz drugi sprawdzi bezpieczeństwo (aby upewnić się, że operacja jest nadal ważna), a następnie wywoła zdarzenie.

Aplikacja może używać wywołania zwrotnego w operacji zapisu, aby upewnić się, że onDisconnect został prawidłowo dołączony:

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

onDisconnect Wydarzenie można też anulować, dzwoniąc pod numer .cancel():

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

Wykrywanie stanu połączenia

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

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

Na Androidzie Firebase automatycznie zarządza stanem połączenia, aby zmniejszyć zużycie przepustowości i baterii. Gdy klient nie ma aktywnych odbiorców, oczekujących operacji zapisu ani operacji onDisconnect i nie został wyraźnie odłączony przez metodę goOffline, Firebase zamyka połączenie po 60 sekundach bezczynności.

Opóźnienie obsługi

Sygnatury czasowe serwera

Serwery Firebase Realtime Database umożliwiają wstawianie sygnatur czasowych generowanych na serwerze jako danych. Ta funkcja w połączeniu z onDisconnect umożliwia łatwe i wiarygodne zapisywanie czasu, w którym klient Realtime Database rozłączył się:

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

Clock Skew

Chociaż firebase.database.ServerValue.TIMESTAMP jest znacznie dokładniejszy i preferowany w przypadku większości operacji odczytu i zapisu, czasami może być przydatne oszacowanie odchylenia zegara klienta względem serwerów Firebase Realtime Database. Możesz dołączyć wywołanie zwrotne do lokalizacji /.info/serverTimeOffset, aby uzyskać wartość w milisekundach, którą Firebase Realtime DatabaseklientyFirebase Realtime Database dodają do lokalnego zgłoszonego czasu (czas epoki w milisekundach), aby oszacować czas serwera. Pamiętaj, że na dokładność tego przesunięcia może wpływać opóźnienie sieci, dlatego jest ono przydatne głównie do wykrywania dużych (powyżej 1 sekundy) rozbieżności w czasie zegarowym.

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

Przykładowa aplikacja Presence

Łącząc operacje rozłączania z monitorowaniem stanu połączenia i znacznikami czasu serwera, możesz utworzyć system obecności użytkowników. W tym systemie każdy użytkownik przechowuje dane w lokalizacji bazy danych, aby wskazać, czy klient Realtime Database jest online. Klienci ustawiają tę lokalizację na wartość „prawda”, gdy są online, a znacznik czasu, gdy się rozłączają. Ta sygnatura czasowa wskazuje, kiedy dany użytkownik był ostatnio online.

Pamiętaj, że aplikacja powinna umieszczać operacje odłączania w kolejce, zanim użytkownik zostanie oznaczony jako online. Pozwoli to uniknąć sytuacji wyścigu w przypadku utraty połączenia sieciowego klienta, zanim oba polecenia zostaną wysłane na serwer.

Oto prosty system obecności użytkownika:

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