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

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

Trwałość dysku

Aplikacje Firebase automatycznie radzą sobie z tymczasowymi przerwami 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 z siecią zostanie przywrócone.

Gdy włączysz trwałość na dysku, aplikacja zapisze dane lokalnie na urządzeniu, aby mogła zachować stan w trybie offline, nawet jeśli użytkownik lub system operacyjny ponownie uruchomi aplikację.

Możesz włączyć trwałość dysku za pomocą jednej linii kodu.

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Zachowanie trwałości

Po włączeniu trwałości wszystkie dane, które klient Firebase Realtime Databasezsynchronizowałby w trybie online, są przechowywane na dysku i są 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 danych lokalnych przechowywanych w pamięci podręcznej. Wywołania zwrotne detektora będą nadal uruchamiane w przypadku aktualizacji lokalnych.

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

Jeśli Twoja aplikacja korzysta z usługi Uwierzytelnianie Firebase, klient Firebase Realtime Database przechowuje token uwierzytelniający użytkownika po ponownym uruchomieniu aplikacji. Jeśli token uwierzytelniania wygaśnie, gdy aplikacja jest offline, klient wstrzyma operacje zapisu do czasu ponownego uwierzytelnienia użytkownika. W przeciwnym razie operacje zapisu mogą się nie udać z powodu reguł zabezpieczeń.

Utrzymywanie aktualności danych

Firebase Realtime Database synchronizuje i przechowuje lokalną kopię danych dla aktywnych słuchaczy. Dodatkowo 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 utrzymuje je w synchronizacji, nawet jeśli referencja nie ma aktywnych słuchaczy. Synchronizację możesz ponownie wyłączyć za pomocą tego wiersza kodu.

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

Domyślnie w pamięci podręcznej jest przechowywanych 10 MB wcześniej zsynchronizowanych danych. Powinna to być wystarczająca ilość miejsca w przypadku większości aplikacji. Jeśli rozmiar pamięci podręcznej 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.

Wysyłanie zapytań do danych offline

Firebase Realtime Database przechowuje dane zwrócone przez zapytanie do użytku w trybie offline. W przypadku zapytań utworzonych w trybie offline obiekt Firebase Realtime Database nadal działa w przypadku danych wczytanych wcześniej. Jeśli żądane dane nie zostały załadowane, Firebase Realtime Database wczyta dane z lokalnej pamięci podręcznej. Gdy połączenie z siecią zostanie przywrócone, dane zostaną załadowane i odzwierciedlą zapytanie.

Ten kod na przykład zwraca 4 ostatnie elementy w tablicy Firebase Realtime Database z wynikami

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ę. W trybie offline aplikacja wysyła zapytanie o 2 ostatnie elementy z tego samego miejsca. To zapytanie zwróci 2 ostatnie pozycje, ponieważ aplikacja wczytała wszystkie 4 pozycje z zapytania powyżej.

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 powyższym przykładzie klient Firebase Realtime Database wywołuje zdarzenia „child added” dla 2 dinozaurów z najwyższą liczbą punktów, korzystając z trwałej pamięci podręcznej. Nie wywoła ono jednak zdarzenia „value”, ponieważ aplikacja nigdy nie wykonała tego zapytania w trybie online.

Jeśli aplikacja poprosi o ostatnie 6 elementów w trybie offline, otrzyma od razu zdarzenia „dodanie dziecka” do tych 4 elementów w pamięci podręcznej. Gdy urządzenie znów połączy się z internetem, klient Firebase Realtime Database zsynchronizuje się z serwerem i otrzyma w aplikacji 2 ostateczne zdarzenia „child added” i „value”.

Obsługa transakcji offline

Wszystkie transakcje przeprowadzane, gdy aplikacja jest offline, są umieszczane w kolejce. Gdy aplikacja ponownie nawiąże połączenie z internetem, transakcje zostaną wysłane do serwera Realtime Database.

Zarządzanie obecnością

W przypadku aplikacji działających w czasie rzeczywistym często przydaje się wykrywanie momentów, w których klienci łączą się z serwerem i z niego rozłączają. Możesz na przykład oznaczyć użytkownika jako użytkownika offline, gdy jego klient się rozłączy.

Klienci bazy danych Firebase udostępniają proste prymitywy, których można używać do zapisywania danych w bazie danych, gdy klient rozłączy się z serwerami bazy danych Firebase. Aktualizacje te są przeprowadzane niezależnie od tego, czy klient się rozłączy, czy nie. Dzięki temu możesz mieć pewność, że wyczyścisz dane nawet w przypadku utraty połączenia lub awarii klienta. Po odłączeniu można wykonywać wszystkie operacje zapisu, w tym ustawianie, aktualizowanie i usuwanie.

Oto prosty przykład zapisywania danych po rozłączeniu za pomocą prymitywu 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

Gdy utworzysz operację onDisconnect(), będzie ona działać na serwerze Firebase Realtime Database. Serwer sprawdza zabezpieczenia, aby upewnić się, że użytkownik może wykonać żądane zdarzenie zapisu, i informuje aplikację, jeśli jest ono nieprawidłowe. Serwer będzie monitorować połączenie. Jeśli w którymkolwiek momencie połączenie przekroczy limit czasu lub zostanie aktywnie zamknięte przez klienta Realtime Database, serwer ponownie sprawdzi zabezpieczenia (aby upewnić się, że operacja jest nadal prawidłowa), a następnie wywoła zdarzenie.

Aplikacja może użyć wywołania zwrotnego operacji zapisu, aby upewnić się, że onDisconnect został prawidłowo dołą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());
        }
    }
});

Wydarzenie onDisconnect można też anulować, dzwoniąc pod numer .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ą aplikacja może wiedzieć, czy jest online czy offline. Firebase Realtime Database udostępnia specjalną lokalizację pod adresem /.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 między klientami Realtime Database, ponieważ zależy od stanu klienta. Innymi słowy, jeśli jeden klient odczytuje wartość /.info/connected jako fałsz, nie ma gwarancji, że inny klient również odczyta wartość jako fałsz.

Na urządzeniach z Androidem Firebase automatycznie zarządza stanem połączenia, aby zmniejszyć wykorzystanie przepustowości i baterii. Gdy klient nie ma aktywnych odbiorców, nie ma oczekujących operacji zapisu ani operacji onDisconnect i nie został wyraźnie odłączony za pomocą metody goOffline, Firebase zamyka połączenie po 60 sekundach bezczynności.

Czas obsługi

Sygnatury czasowe serwera

Serwery Firebase Realtime Database udostępniają mechanizm wstawiania sygnatur czasowych wygenerowanych na serwerze jako danych. Ta funkcja w połączeniu z funkcją onDisconnect umożliwia łatwe zanotowanie godziny rozłączenia klienta 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);

Zniekształcenie zegara

Funkcja firebase.database.ServerValue.TIMESTAMP jest znacznie dokładniejsza i preferowana w przypadku większości operacji odczytu/zapisu, ale czasami może być przydatna do oszacowania 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ą klienty Firebase Realtime Database dodają do lokalnego czasu zgłaszanego (w milisekundach) w celu oszacowania czasu serwera. Pamiętaj, że dokładność tego przesunięcia może być zależna od opóźnienia w sieci, dlatego jest przydatna głównie do wykrywania dużych (> 1 sekunda) 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 do obsługi stanu obecności

Łącząc operacje rozłączania z monitorowaniem stanu połączenia i sygnaturami czasowymi serwera, możesz utworzyć system wykrywania 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 „Prawda” (true) po połączeniu z internetem i dodają sygnaturę czasową po rozłączeniu. Ta sygnatura czasowa wskazuje, kiedy dany użytkownik był ostatnio online.

Pamiętaj, że aplikacja powinna kolejkować operacje odłączania, zanim użytkownik zostanie oznaczony jako online. Pozwoli to uniknąć wyścigów w przypadku utraty połączenia sieciowego klienta przed wysłaniem obu poleceń na serwer.

Oto prosty system wykrywania 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");
    }
});