Abilitazione delle funzionalità offline su Android

Le applicazioni Firebase funzionano anche se la tua 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 eventuali scritture quando viene ripristinata la connettività di rete.

Quando abiliti la persistenza del disco, la tua app scrive i dati localmente sul dispositivo in modo che possa mantenere lo stato mentre è offline, anche se l'utente o il sistema operativo riavvia l'app.

Puoi abilitare la persistenza del disco con una sola riga di codice.

Kotlin+KTX

Firebase.database.setPersistenceEnabled(true)

Java

FirebaseDatabase.getInstance().setPersistenceEnabled(true);

Comportamento di persistenza

Abilitando la persistenza, tutti i dati che il client Firebase Realtime Database sincronizzerà mentre è online persistono su 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. Le richiamate degli ascoltatori continueranno a 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 scritture siano disponibili quando l'utente o il sistema operativo riavvia l'app. Quando l'app riacquista 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 durante i riavvii dell'app. Se il token di autenticazione scade mentre l'app è offline, il client sospende 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

Il Firebase Realtime Database sincronizza e archivia una copia locale dei dati per gli ascoltatori attivi. Inoltre, puoi mantenere sincronizzate posizioni specifiche.

Kotlin+KTX

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 ascoltatori attivi. È possibile disattivare nuovamente la sincronizzazione con la seguente riga di codice.

Kotlin+KTX

scoresRef.keepSynced(false)

Java

scoresRef.keepSynced(false);

Per impostazione predefinita, vengono memorizzati nella cache 10 MB di dati precedentemente sincronizzati. 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 mantenuti sincronizzati non vengono eliminati dalla cache.

Interrogazione dei dati offline

Il Firebase Realtime Database archivia i dati restituiti da una query per utilizzarli offline. Per le query costruite 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 connettività di rete sarà nuovamente disponibile, i dati verranno caricati e rifletteranno la query.

Ad esempio, questo codice esegue una query sugli ultimi quattro elementi in un database di punteggi Firebase Realtime

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

    // ...
});

Supponiamo che l'utente perda la connessione, vada offline e riavvii l'app. Mentre sei ancora offline, l'app ricerca gli ultimi due elementi dalla stessa posizione. Questa query restituirà correttamente gli ultimi due elementi perché l'app ha caricato tutti e quattro gli elementi nella query precedente.

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

    // ...
});

Nell'esempio precedente, il client Firebase Realtime Database genera eventi "figli aggiunti" per i due dinosauri con il punteggio più alto, utilizzando la cache persistente. Ma non genererà un evento "valore", poiché l'app non ha mai eseguito quella query mentre era online.

Se l'app richiedesse gli ultimi sei elementi mentre è offline, otterrebbe immediatamente eventi "aggiunti secondari" per i quattro elementi memorizzati nella cache. Quando il dispositivo torna online, il client Firebase Realtime Database si sincronizza con il server e ottiene gli ultimi due eventi "secondario aggiunto" e "valore" per l'app.

Gestione delle transazioni offline

Tutte le transazioni eseguite mentre l'app è offline vengono accodate. Una volta che l'app riacquista la connettività di rete, le transazioni vengono inviate al server Realtime Database.

Gestire la 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 del database Firebase forniscono semplici primitive che puoi utilizzare per scrivere nel database quando un client si disconnette dai server del database Firebase. Questi aggiornamenti si verificano indipendentemente dal fatto che il client si disconnetta in modo pulito 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, incluse l'impostazione, l'aggiornamento e la rimozione, possono essere eseguite dopo una disconnessione.

Ecco un semplice esempio di scrittura dei dati al momento della disconnessione utilizzando la primitiva 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!");

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 l'app se non è valido. Il server quindi monitora 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 quindi richiama l'evento.

La tua app può utilizzare la richiamata sull'operazione di scrittura per garantire che onDisconnect sia stato collegato correttamente:

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

Un evento onDisconnect può anche essere annullato chiamando .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();

Rilevamento dello stato della connessione

Per molte funzionalità relative alla presenza, è utile che l'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 di connessione del client Firebase Realtime Database. Ecco un esempio:

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 è un valore booleano che non è sincronizzato tra i client Realtime Database poiché il valore dipende dallo stato del client. In altre parole, se un client legge /.info/connected come false, ciò non garantisce che anche un client separato leggerà false.

Su Android, Firebase gestisce automaticamente lo stato della connessione per ridurre la larghezza di banda e l'utilizzo della batteria. Quando un client non ha ascoltatori attivi, nessuna operazione di scrittura o onDisconnect in sospeso e non è esplicitamente disconnesso 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 timestamp generati sul server come dati. Questa funzionalità, combinata con onDisconnect , fornisce un modo semplice per prendere nota in modo affidabile dell'ora in cui un client Realtime Database si è disconnesso:

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

Disallineamento dell'orologio

Sebbene firebase.database.ServerValue.TIMESTAMP sia molto più accurato e preferibile per la maggior parte delle operazioni di lettura/scrittura, a volte può essere utile per stimare lo sfasamento dell'orologio del client rispetto ai server di Firebase Realtime Database. Puoi allegare un callback alla posizione /.info/serverTimeOffset per ottenere il valore, in millisecondi, che i client Firebase Realtime Database aggiungono all'ora locale riportata (tempo epocale in millisecondi) per stimare l'ora del server. Tieni presente che la precisione di questo offset può essere influenzata dalla latenza della rete e quindi è utile principalmente per scoprire discrepanze ampie (> 1 secondo) nell'orario dell'orologio.

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

Applicazione campione di presenza

Combinando le operazioni di disconnessione con il monitoraggio dello stato della connessione e i timestamp del server, è possibile creare un sistema di presenza dell'utente. In questo sistema, ciascun utente memorizza 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 sono online e un timestamp quando si disconnettono. Questo timestamp indica l'ultima volta che un determinato utente è stato online.

Tieni presente che la tua app deve accodare le operazioni di disconnessione prima che un utente venga contrassegnato online, per evitare eventuali condizioni di competizione 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 utente:

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