Włączanie funkcji offline w systemie Android

Aplikacje Firebase działają nawet wtedy, gdy Twoja aplikacja chwilowo traci połączenie sieciowe. Ponadto Firebase zapewnia 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 działaniu sieci. Dane z pamięci podręcznej są dostępne w trybie offline, a Firebase ponownie wysyła wszelkie zapisy po przywróceniu łączności sieciowej.

Po włączeniu trwałości dysku aplikacja zapisuje dane lokalnie na urządzeniu, dzięki czemu może zachowywać 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łości

Włączając trwałość, wszelkie dane, które klient Bazy danych czasu rzeczywistego Firebase zsynchronizowałby w trybie online, pozostają na dysku i są dostępne w trybie offline, nawet po ponownym uruchomieniu aplikacji przez użytkownika lub system operacyjny. Oznacza to, że Twoja aplikacja działa tak, jak w trybie online, korzystając z lokalnych danych przechowywanych w pamięci podręcznej. Wywołania zwrotne odbiornika będą nadal uruchamiane w celu uzyskania lokalnych aktualizacji.

Klient bazy danych czasu rzeczywistego Firebase automatycznie tworzy kolejkę wszystkich operacji zapisu, które są wykonywane, gdy aplikacja jest offline. Po włączeniu trwałości ta kolejka jest również utrwalana na dysku, dzięki czemu wszystkie zapisy są dostępne, gdy użytkownik lub system operacyjny ponownie uruchomi aplikację. Gdy aplikacja odzyska łączność, wszystkie operacje są wysyłane do serwera Bazy danych czasu rzeczywistego Firebase.

Jeśli Twoja aplikacja korzysta z uwierzytelniania Firebase , klient Bazy danych czasu rzeczywistego Firebase zachowuje token uwierzytelniania użytkownika podczas ponownego uruchamiania aplikacji. Jeśli token uwierzytelniania wygaśnie, gdy aplikacja jest w trybie offline, klient wstrzymuje operacje zapisu do czasu ponownego uwierzytelnienia użytkownika przez aplikację. W przeciwnym razie operacje zapisu mogą zakończyć się niepowodzeniem ze względu na reguły zabezpieczeń.

Utrzymywanie aktualności danych

Baza danych czasu rzeczywistego Firebase 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 utrzymuje je w synchronizacji, nawet jeśli odwołanie nie ma aktywnych słuchaczy. Możesz ponownie wyłączyć synchronizację za pomocą następującego wiersza kodu.

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

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

Wyszukiwanie danych w trybie offline

Baza danych czasu rzeczywistego Firebase przechowuje dane zwrócone z zapytania do wykorzystania w trybie offline. W przypadku zapytań tworzonych w trybie offline baza danych czasu rzeczywistego Firebase nadal działa dla wcześniej załadowanych danych. Jeśli żądane dane nie zostały załadowane, baza danych czasu rzeczywistego Firebase ładuje 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 ostatnie cztery elementy w bazie danych wyników w czasie rzeczywistym 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());
    }

    // ...
});

Załóżmy, że użytkownik traci połączenie, przechodzi w tryb offline i ponownie uruchamia aplikację. 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 czasu rzeczywistego Firebase zgłasza zdarzenia „dodania elementu podrzędnego” dla dwóch dinozaurów o najwyższej punktacji, korzystając z utrwalonej pamięci podręcznej. Nie wywoła to jednak zdarzenia „wartość”, ponieważ aplikacja nigdy nie wykonała tego zapytania w trybie online.

Gdyby aplikacja zażądała ostatnich sześciu elementów w trybie offline, od razu otrzymałaby zdarzenia „dodania dziecka” dla czterech elementów zapisanych w pamięci podręcznej. Gdy urządzenie wraca do trybu online, klient bazy danych czasu rzeczywistego Firebase synchronizuje się z serwerem i pobiera ostatnie dwa zdarzenia „dodane dziecko” i „wartość” dla aplikacji.

Obsługa transakcji offline

Wszelkie transakcje wykonywane, gdy aplikacja jest w trybie offline, są umieszczane w kolejce. Gdy aplikacja odzyska łączność sieciową, transakcje są wysył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ą. Na przykład możesz chcieć oznaczyć użytkownika jako „offline”, gdy jego klient rozłączy się.

Klienci bazy danych Firebase udostępniają proste operacje podstawowe, których można użyć do zapisu w bazie danych, gdy klient rozłączy się z serwerami bazy danych Firebase. Aktualizacje te występują niezależnie od tego, czy klient rozłączy się bezproblemowo, czy nie, więc możesz polegać na nich w czyszczeniu danych, nawet jeśli połączenie zostanie zerwane lub klient ulegnie awarii. 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 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

Po ustanowieniu operacji onDisconnect() operacja działa na serwerze bazy danych czasu rzeczywistego Firebase. Serwer sprawdza zabezpieczenia, aby upewnić się, że użytkownik może wykonać żądane zdarzenie zapisu, i informuje aplikację, jeśli jest 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 bazy danych czasu rzeczywistego, serwer sprawdza zabezpieczenia po raz drugi (aby upewnić się, że operacja jest nadal prawidłowa), a następnie wywołuje zdarzenie.

Twoja aplikacja może użyć wywołania zwrotnego w operacji zapisu, aby upewnić się, że onDisconnect został prawidłowo 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ą przydatna jest informacja, czy aplikacja jest w trybie online, czy offline. Baza danych czasu rzeczywistego Firebase zapewnia specjalną lokalizację w /.info/connected , która jest aktualizowana za każdym razem, gdy zmienia się stan połączenia klienta Bazy danych czasu rzeczywistego Firebase. 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 bazy danych czasu rzeczywistego, ponieważ wartość zależy od stanu klienta. Innymi słowy, jeśli jeden klient odczyta /.info/connected jako fałsz, nie ma gwarancji, że inny klient również odczyta fałsz.

W systemie Android Firebase automatycznie zarządza stanem połączenia, aby zmniejszyć przepustowość i zużycie baterii. Gdy klient nie ma aktywnych odbiorników, żadnych oczekujących operacji zapisu ani operacji onDisconnect i nie jest jawnie rozłączony za pomocą metody goOffline , Firebase zamyka połączenie po 60 sekundach bezczynności.

Obsługa opóźnienia

Znaczniki czasu serwera

Serwery bazy danych czasu rzeczywistego Firebase zapewniają mechanizm wstawiania znaczników czasu generowanych na serwerze jako dane. Ta funkcja, w połączeniu z onDisconnect , zapewnia łatwy sposób niezawodnego zanotowania czasu, w którym klient bazy danych czasu rzeczywistego rozłączył się:

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

Podczas gdy firebase.database.ServerValue.TIMESTAMP jest znacznie dokładniejszy i preferowany w przypadku większości operacji odczytu/zapisu, czasami może być przydatny do oszacowania przesunięcia zegara klienta w odniesieniu do serwerów bazy danych czasu rzeczywistego Firebase. Możesz dołączyć wywołanie zwrotne do lokalizacji /.info/serverTimeOffset , aby uzyskać wartość w milisekundach, którą klienci Bazy danych czasu rzeczywistego Firebase dodają do lokalnego zgłaszanego czasu (czas epoki w milisekundach) w celu oszacowania czasu serwera. Należy zauważyć, że na dokładność tego przesunięcia może mieć wpływ 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 do sprawdzania obecności

Łącząc operacje rozłączania z monitorowaniem stanu połączenia i znacznikami czasu serwera, można 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 true, gdy przechodzą do trybu online, i znacznik czasu, gdy się rozłączają. Ten znacznik czasu wskazuje ostatni raz, kiedy dany użytkownik był online.

Pamiętaj, że Twoja aplikacja powinna umieszczać w kolejce operacje rozłączenia, zanim użytkownik zostanie oznaczony jako online, aby uniknąć warunków wyścigu w przypadku utraty połączenia sieciowego klienta, zanim oba polecenia będą mogły zostać wysłane do serwera.

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