Sur une Android qui doit fonctionner hors ligne la plupart du temps dont j'ai besoin, lorsqu'elle est en ligne, pour effectuer des opérations synchrones, par exemple:
User myUser = MyclientFacade.getUser();
If (myUser.getScore > 10) {
DoSomething()
}
Où l'utilisateur est un POJO rempli par Firebase;
Le problème se produit lorsque le cache Firebase est activé
Firebase.getDefaultConfig().setPersistenceEnabled(true);
et l'utilisateur est déjà dans le cache et les données sont mises à jour sur Firebase DB par un tiers (ou même par un autre appareil). En effet, lorsque je demande à Firebase d'obtenir l'utilisateur, j'obtiens d'abord les données du cache et ensuite un deuxième événement de modification avec les dernières données du serveur Firebase, mais c'est trop tard!
Voyons la méthode synchrone MyclientFacade.getUser ():
Public User getUser() {
Firebase ref = myFireBaseroot.child("User").child(uid);
ref.keepSynced(true);
/* try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
final CountDownLatch signal = new CountDownLatch(1);
ref.addListenerForSingleValueEvent(new ValueEventListener() {
//ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
this.user = dataSnapshot.getValue(User.class);
signal.countDown();
}
@Override
public void onCancelled(FirebaseError firebaseError) {
signal.countDown();
}
});
signal.await(5, TimeUnit.SECONDS);
ref.keepSynced(false);
return this.user;
}
J'obtiens le même comportement si j'utilise addValueEventListener
ou addListenerForSingleValueEvent
mélangé avec ref.keepSynced
:
Disons que la valeur de score de mon utilisateur dans le cache est de 5 et de Firebase DB est de 11.
Lorsque j'appelle getUser
j'obtiendrai le score de 5 (Firebase ask cache first) donc je n'appellerai pas la méthode doSomething()
.
Si je décommente le code Thread.sleep()
de mon exemple, le cache Firebase aura suffisamment de temps pour être mis à jour et mon getUser
renverra la valeur de score correcte (11).
Alors, comment puis-je demander directement la dernière valeur directement du côté serveur et contourner le cache?
C'était un problème qui me causait aussi beaucoup de stress dans ma candidature.
J'ai tout essayé, de la modification de .addListenerForSingleValueEvent()
à .addValueEventListener()
à l'utilisation créative de .keepSynced()
à l'utilisation d'un délai (la méthode Thread.sleep()
que vous avez décrite ci-dessus) et rien n'a vraiment fonctionné de manière cohérente (même la méthode Thread.sleep()
, qui n'était pas vraiment acceptable dans une application de production ne m'a pas donné de résultats cohérents).
Donc, ce que j'ai fait était la suivante: après avoir créé un objet Query et appelé .keepSynced()
dessus, je procède ensuite à l'écriture d'un objet maquette/jeton dans le nœud que j'interroge et ALORS dans l'écouteur d'achèvement de cette opération, je fais la récupération de données que je veux faire, après avoir supprimé l'objet factice.
Quelque chose comme:
MockObject mock = new MockObject();
mock.setIdentifier("delete!");
final Query query = firebase.child("node1").child("node2");
query.keepSynced(true);
firebase.child("node1").child("node2").child("refreshMock")
.setValue(mock, new CompletionListener() {
@Override
public void onComplete(FirebaseError error, Firebase afb) {
query.addListenerForSingleValueEvent(new ValueEventListener() {
public void onDataChange(DataSnapshot data) {
// get identifier and recognise that this data
// shouldn't be used
// it's also probably a good idea to remove the
// mock object
// using the removeValue() method in its
// speficic node so
// that the database won't be saddled with a new
// one in every
// read operation
}
public void onCancelled(FirebaseError error) {
}
});
}
});
}
Jusqu'à présent, cela a toujours fonctionné pour moi! (eh bien, pendant un jour, prenez-le avec un grain de sel). Il semble que faire une opération d'écriture avant de lire contourne le cache, ce qui est logique. Ainsi, les données reviennent fraîches.
Le seul inconvénient est l'opération d'écriture supplémentaire avant l'opération de lecture, ce qui peut provoquer un petit retard (évidemment utiliser un petit objet) mais si c'est le prix pour des données toujours fraîches, je le prendrai!
J'espère que cela t'aides!
Une solution de contournement que j'ai découverte utilise la méthode runTransaction()
de Firebase. Cela semble toujours récupérer les données du serveur.
String firebaseUrl = "/some/user/datapath/";
final Firebase firebaseClient = new Firebase(firebaseUrl);
// Use runTransaction to bypass cached DataSnapshot
firebaseClient.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
// Return passed in data
return Transaction.success(mutableData);
}
@Override
public void onComplete(FirebaseError firebaseError, boolean success, DataSnapshot dataSnapshot) {
if (firebaseError != null || !success || dataSnapshot == null) {
System.out.println("Failed to get DataSnapshot");
} else {
System.out.println("Successfully get DataSnapshot");
//handle data here
}
}
});
Ajoutez simplement ce code sur la méthode onCreate
sur votre classe d'application . (changer la référence de la base de données)
Exemple:
public class MyApplication extendes Application{
@Override
public void onCreate() {
super.onCreate();
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);
}
}
Fonctionne bien pour moi.
Référence: https://firebase.google.com/docs/database/Android/offline-capabilities
Ma solution était d'appeler Database.database (). IsPersistenceEnabled = true lors du chargement, puis d'appeler .keepSynced (true) sur tous les nœuds que j'avais besoin d'actualiser.
Le problème ici est que si vous interrogez votre nœud juste après .keepSynced (true), vous obtiendrez probablement le cache plutôt que les nouvelles données. Contournement légèrement boiteux mais fonctionnel: retardez votre requête du nœud pendant une seconde environ afin de donner à Firebase un certain temps pour obtenir les nouvelles données. Vous obtiendrez le cache à la place si l'utilisateur est hors ligne.
Oh, et si c'est un nœud que vous ne voulez pas garder à jour en arrière-plan pour toujours, n'oubliez pas d'appeler .keepSynced (false) lorsque vous avez terminé.
J'ai essayé les deux solutions acceptées et j'ai essayé la transaction. La solution de transaction est plus propre et plus agréable, mais le succès OnComplete n'est appelé que lorsque db est en ligne, vous ne pouvez donc pas charger à partir du cache. Mais vous pouvez annuler la transaction, puis onComplete sera également appelé hors ligne (avec les données mises en cache).
J'ai précédemment créé une fonction qui ne fonctionnait que si la base de données obtenait suffisamment de connexions pour effectuer la synchronisation. J'ai résolu le problème en ajoutant un délai d'expiration. Je vais y travailler et tester si cela fonctionne. Peut-être qu'à l'avenir, quand j'aurai du temps libre, je créerai Android lib et le publierai, mais d'ici là, c'est le code dans 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<T>()
data?.let {
data.children.forEach {
val child = it.getValue(aClass)
child?.let {
listOfObjects.add(child)
}
}
}
callback.invoke(listOfObjects, true)
removeListener()
}
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<T>()
data?.let {
data.children.forEach {
val child = it.getValue(aClass)
child?.let {
listOfObjects.add(child)
}
}
}
callback.invoke(listOfObjects, false)
removeListener()
}
override fun doTransaction(p0: MutableData?): Transaction.Result {
return Transaction.success(p0)
}
}
Si vous souhaitez accélérer la connexion hors ligne (pour ne pas attendre bêtement avec un délai d'expiration lorsque la base de données n'est évidemment pas connectée), vérifiez si la base de données est connectée avant d'utiliser la fonction ci-dessus:
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");
}
});