Le applicazioni Firebase funzionano anche se l'app perde temporaneamente la connessione di rete. Inoltre, Firebase fornisce strumenti per la persistenza dei dati a livello locale, la gestione della presenza e la gestione della latenza.
Persistenza del disco
Le app Firebase gestiscono automaticamente le interruzioni temporanee della rete. I dati memorizzati nella cache sono disponibili offline e Firebase invia nuovamente le scritture quando viene ripristinata la connessione di rete.
Quando attivi la persistenza del disco, l'app scrive i dati localmente sul dispositivo in modo da poter mantenere lo stato mentre è offline, anche se l'utente o il sistema operativo riavvia l'app.
Puoi attivare la persistenza del disco con una sola riga di codice.
Kotlin
Firebase.database.setPersistenceEnabled(true)
Java
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
Comportamento di persistenza
Se abiliti la persistenza, tutti i dati che il client Firebase Realtime Database sincronizzerebbe mentre è online vengono mantenuti sul disco e sono disponibili offline, anche quando l'utente o il sistema operativo riavvia l'app. Ciò significa che la tua app funziona come se fosse online utilizzando i dati locali archiviati nella cache. I callback dei listener continueranno ad attivarsi per gli aggiornamenti locali.
Il client Firebase Realtime Database mantiene automaticamente una coda di tutte le operazioni di scrittura eseguite mentre l'app è offline. Quando la persistenza è abilitata, questa coda viene mantenuta anche sul disco, in modo che tutte le tue scritture siano disponibili quando l'utente o il sistema operativo riavvia l'app. Quando l'app recupera la connettività, tutte le operazioni vengono inviate al server Firebase Realtime Database.
Se la tua app utilizza Firebase Authentication, il client Firebase Realtime Database mantiene il token di autenticazione dell'utente tra i riavvii dell'app. Se il token di autenticazione scade mentre l'app è offline, il client mette in pausa le operazioni di scrittura finché l'app non autentica nuovamente l'utente, altrimenti le operazioni di scrittura potrebbero non riuscire a causa delle regole di sicurezza.
Mantenere i dati aggiornati
Firebase Realtime Database sincronizza e archivia una copia locale dei dati per gli ascoltatori attivi. Inoltre, puoi mantenere sincronizzate località specifiche.
Kotlin
val scoresRef = Firebase.database.getReference("scores") scoresRef.keepSynced(true)
Java
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores"); scoresRef.keepSynced(true);
Il client Firebase Realtime Database scarica automaticamente i dati in queste posizioni e li mantiene sincronizzati anche se il riferimento non ha listener attivi. Puoi disattivare nuovamente la sincronizzazione con la seguente riga di codice.
Kotlin
scoresRef.keepSynced(false)
Java
scoresRef.keepSynced(false);
Per impostazione predefinita, vengono memorizzati nella cache 10 MB di dati sincronizzati in precedenza. Questo dovrebbe essere sufficiente per la maggior parte delle applicazioni. Se la cache supera le dimensioni configurate, Firebase Realtime Database elimina i dati utilizzati meno di recente. I dati sincronizzati non vengono eliminati dalla cache.
Esecuzione di query sui dati offline
Firebase Realtime Database archivia i dati restituiti da una query per l'utilizzo offline. Per le query create offline, Firebase Realtime Database continua a funzionare per i dati caricati in precedenza. Se i dati richiesti non sono stati caricati, Firebase Realtime Database carica i dati dalla cache locale. Quando la connessione di rete sarà nuovamente disponibile, i dati verranno caricati e rifletteranno la query.
Ad esempio, questo codice esegue una query per gli ultimi quattro elementi in un Firebase Realtime Database di punteggi
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()); } // ... });
Supponiamo che l'utente perda la connessione, vada offline e riavvii l'app. Mentre è ancora offline, l'app esegue una query per gli ultimi due elementi dalla stessa posizione. Questa query restituirà correttamente gli ultimi due elementi perché l'app aveva caricato tutti e quattro gli elementi nella query precedente.
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()); } // ... });
Nell'esempio precedente, il client Firebase Realtime Database genera eventi "child_added" per i due dinosauri con il punteggio più alto utilizzando la cache persistente. Tuttavia, non genererà un evento "value", poiché l'app non ha mai eseguito la query mentre era online.
Se l'app dovesse richiedere gli ultimi sei elementi mentre è offline, riceverebbe immediatamente eventi "child_added" per i quattro elementi memorizzati nella cache. Quando il dispositivo torna online, il client Firebase Realtime Database si sincronizza con il server e riceve gli ultimi due eventi "child_added" e l'evento "value" per l'app.
Gestione delle transazioni offline
Tutte le transazioni eseguite mentre l'app è offline vengono messe in coda. Una volta ripristinata la connettività di rete dell'app, le transazioni vengono inviate al server Realtime Database.
Gestione della presenza
Nelle applicazioni in tempo reale è spesso utile rilevare quando i client si connettono e si disconnettono. Ad esempio, potresti voler contrassegnare un utente come "offline" quando il suo client si disconnette.
I client Firebase Database forniscono primitive semplici che puoi utilizzare per scrivere nel database quando un client si disconnette dai server Firebase Database. Questi aggiornamenti si verificano indipendentemente dal fatto che il client si disconnetta correttamente o meno, quindi puoi fare affidamento su di essi per ripulire i dati anche se una connessione viene interrotta o un client si arresta in modo anomalo. Tutte le operazioni di scrittura, tra cui l'impostazione, l'aggiornamento e la rimozione, possono essere eseguite in caso di disconnessione.
Ecco un semplice esempio di scrittura dei dati al momento della disconnessione utilizzando la primitiva 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!");
Come funziona onDisconnect
Quando stabilisci un'operazione onDisconnect(), l'operazione
risiede sul server Firebase Realtime Database. Il server controlla la sicurezza per assicurarsi che l'utente possa eseguire l'evento di scrittura richiesto e informa la tua app se non è valido. Il server monitora quindi la connessione. Se in qualsiasi momento la connessione scade o viene chiusa attivamente dal client Realtime Database, il server controlla la sicurezza una seconda volta (per assicurarsi che l'operazione sia ancora valida) e poi richiama l'evento.
La tua app può utilizzare il callback nell'operazione di scrittura per assicurarsi che onDisconnect sia stato collegato correttamente:
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()); } } });
Un evento onDisconnect può essere annullato anche chiamando il numero .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();
Rilevamento dello stato della connessione
Per molte funzionalità relative alla presenza, è utile che la tua app sappia quando è online o offline. Firebase Realtime Database
fornisce una posizione speciale in /.info/connected che
viene aggiornata ogni volta che cambia lo stato della connessione del client
Firebase Realtime Database. Ecco un esempio:
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 è un valore booleano che non viene sincronizzato tra i client Realtime Database perché il valore dipende dallo stato del client. In altre parole, se un client legge /.info/connected come false, non è garantito che un altro client legga anch'esso false.
Su Android, Firebase gestisce automaticamente lo stato della connessione per ridurre l'utilizzo della larghezza di banda e della batteria. Quando un client non ha listener attivi,
nessuna operazione di scrittura o onDisconnect
in attesa e non viene disconnesso esplicitamente dal
metodo goOffline,
Firebase chiude la connessione dopo 60 secondi di inattività.
Gestione della latenza
Timestamp del server
I server Firebase Realtime Database forniscono un meccanismo per inserire i timestamp generati sul server come dati. Questa funzionalità, combinata con onDisconnect, offre un modo semplice per annotare in modo affidabile l'ora in cui un client Realtime Database si è disconnesso:
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);
Disallineamento del clock
Sebbene firebase.database.ServerValue.TIMESTAMP sia molto più preciso e preferibile per la maggior parte delle operazioni di lettura/scrittura, a volte può essere utile stimare lo sfasamento dell'orologio del client rispetto ai server di Firebase Realtime Database. Puoi
collegare un callback alla posizione /.info/serverTimeOffset
per ottenere il valore, in millisecondi, che i client Firebase Realtime Database
aggiungono all'ora locale registrata (ora epoch in millisecondi) per stimare
l'ora del server. Tieni presente che l'accuratezza di questo offset può essere influenzata dalla latenza di rete, pertanto è utile principalmente per rilevare discrepanze di grandi dimensioni (> 1 secondo) nell'orario.
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"); } });
App di esempio per la presenza
Combinando le operazioni di disconnessione con il monitoraggio dello stato della connessione e i timestamp del server, puoi creare un sistema di presenza degli utenti. In questo sistema, ogni utente archivia i dati in una posizione del database per indicare se un client Realtime Database è online o meno. I client impostano questa posizione su true quando vanno online e un timestamp quando si disconnettono. Questo timestamp indica l'ultima volta che l'utente in questione è stato online.
Tieni presente che l'app deve mettere in coda le operazioni di disconnessione prima che un utente venga contrassegnato come online, per evitare eventuali race condition nel caso in cui la connessione di rete del client venga persa prima che entrambi i comandi possano essere inviati al server.
Ecco un semplice sistema di presenza dell'utente:
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"); } });