Firebase Offline Capabilities e addListenerForSingleValueEvent

Ogni volta che uso addListenerForSingleValueEvent con setPersistenceEnabled(true) , riesco solo a ottenere una copia locale offline di DataSnapshot e NON il DataSnapshot aggiornato dal server.

Tuttavia, se uso addValueEventListener con setPersistenceEnabled(true) , posso ottenere l’ultima copia di DataSnapshot dal server.

È normale per addListenerForSingleValueEvent poiché esegue la ricerca solo su DataSnapshot localmente (offline) e rimuove il relativo listener dopo aver recuperato DataSnapshot ONCE (offline o online)?

Come funziona la persistenza

Il client Firebase conserva una copia di tutti i dati che stai ascoltando triggersmente in memoria. Quando l’ultimo ascoltatore si disconnette, i dati vengono scaricati dalla memoria.

Se attivi la persistenza del disco in un’applicazione Android Firebase con:

 Firebase.getDefaultConfig().setPersistenceEnabled(true); 

Il client Firebase manterrà una copia locale (su disco) di tutti i dati che l’app ha recentemente ascoltato.

Cosa succede quando si allega un ascoltatore

Supponiamo di avere il seguente ValueEventListener :

 ValueEventListener listener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { System.out.println(snapshot.getValue()); } @Override public void onCancelled(FirebaseError firebaseError) { // No-op } }; 

Quando aggiungi un ValueEventListener a un percorso:

 ref.addValueEventListener(listener); // OR ref.addListenerForSingleValueEvent(listener); 

Se il valore del percorso si trova nella cache del disco locale, il client Firebase invocherà onDataChange() immediatamente per quel valore dalla cache locale. Se poi avvierà anche un controllo con il server, per chiedere eventuali aggiornamenti del valore. Può successivamente richiamare onDataChange() nuovo se c’è stata una modifica dei dati sul server dall’ultima volta che è stata aggiunta alla cache.

Cosa succede quando si utilizza addListenerForSingleValueEvent

Quando aggiungi un listener di eventi a valore singolo nella stessa posizione:

 ref.addListenerForSingleValueEvent(listener); 

Il client Firebase (come nella situazione precedente) invocherà immediatamente onDataChange() per il valore dalla cache del disco locale. Non invocherà più onDataChange() anche se il valore sul server risulta diverso. Si noti che i dati aggiornati verranno comunque richiesti e restituiti in caso di richieste successive.

Questo è stato trattato in precedenza in Come funziona la sincronizzazione di Firebase, con dati condivisi?

Soluzione e soluzione

La soluzione migliore è utilizzare addValueEventListener() , anziché un listener di eventi a valore singolo. Un listener di valori regolari otterrà sia l’evento locale immediato che il potenziale aggiornamento dal server.

Come soluzione alternativa puoi anche chiamare keepSynced(true) nelle posizioni in cui utilizzi un listener di eventi a valore singolo. Ciò garantisce che i dati vengano aggiornati ogni volta che cambia, il che aumenta drasticamente la possibilità che il listener di eventi a valore singolo visualizzi il valore corrente.

È ansible creare una transazione e interromperla, quindi onComplete verrà chiamato quando online (dati netti) o offline (dati memorizzati nella cache)

In precedenza avevo creato una funzione che funzionava solo se il database ha avuto abbastanza connessione per fare la sincronizzazione. Ho risolto il problema aggiungendo il timeout. Lavorerò su questo e testerò se funziona. Forse in futuro, quando avrò tempo libero, creerò la lib Android e la pubblicherò, ma a quel punto è il codice in kotlin:

 /** * @param databaseReference reference to parent database node * @param callback callback with mutable list which returns list of objects and boolean if data is from cache * @param timeOutInMillis if not set it will wait all the time to get data online. If set - when timeout occurs it will send data from cache if exists */ fun readChildrenOnlineElseLocal(databaseReference: DatabaseReference, callback: ((mutableList: MutableList< @kotlin.UnsafeVariance T>, isDataFromCache: Boolean) -> Unit), timeOutInMillis: Long? = null) { var countDownTimer: CountDownTimer? = null val transactionHandlerAbort = object : Transaction.Handler { //for cache load override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) { val listOfObjects = ArrayList() data?.let { data.children.forEach { val child = it.getValue(aClass) child?.let { listOfObjects.add(child) } } } callback.invoke(listOfObjects, true) } override fun doTransaction(p0: MutableData?): Transaction.Result { return Transaction.abort() } } val transactionHandlerSuccess = object : Transaction.Handler { //for online load override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) { countDownTimer?.cancel() val listOfObjects = ArrayList() data?.let { data.children.forEach { val child = it.getValue(aClass) child?.let { listOfObjects.add(child) } } } callback.invoke(listOfObjects, false) } override fun doTransaction(p0: MutableData?): Transaction.Result { return Transaction.success(p0) } } 

Nel codice se è impostato il timeout, imposto il timer che chiamerà la transazione con l’interruzione. Questa transazione verrà chiamata anche quando non in linea e fornirà dati in linea o memorizzati nella cache (in questa funzione è molto probabile che questi dati siano memorizzati nella cache). Quindi chiamo transazione con successo. OnComplete verrà chiamato SOLO se otteniamo risposta dal database di Firebase. Ora possiamo annullare il timer (se non è null) e inviare i dati al callback.

Questa implementazione rende sicuro al 99% che i dati provengano dalla cache o siano online.

Se vuoi renderlo più veloce per offline (non aspettare stupidamente con timeout quando ovviamente il database non è connesso) quindi controlla se il database è collegato prima di usare la funzione sopra:

 DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected"); connectedRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot snapshot) { boolean connected = snapshot.getValue(Boolean.class); if (connected) { System.out.println("connected"); } else { System.out.println("not connected"); } } @Override public void onCancelled(DatabaseError error) { System.err.println("Listener was cancelled"); } }); 

Durante il workink con la persistenza abilitata, ho contato le volte in cui il listener ha ricevuto una chiamata a onDataChange () e si è fermata ad ascoltare 2 volte. Ha funzionato per me, forse aiuta:

 private int timesRead; private ValueEventListener listener; private DatabaseReference ref; private void readFB() { timesRead = 0; if (ref == null) { ref = mFBDatabase.child("URL"); } if (listener == null) { listener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { //process dataSnapshot timesRead++; if (timesRead == 2) { ref.removeEventListener(listener); } } @Override public void onCancelled(DatabaseError databaseError) { } }; } ref.removeEventListener(listener); ref.addValueEventListener(listener); }