41

On an Android application which must works offline most of the time I need, when it's online, to do some synchronous operations for i.e. :

User myUser =  MyclientFacade.getUser();
If (myUser.getScore > 10) {
    DoSomething() 
}

Where User is a POJO filled by Firebase;

The problem occurs when the Firebase cache is activated

Firebase.getDefaultConfig().setPersistenceEnabled(true);

and the user is already in cache and the data are updated on Firebase DB by a third party (or even by another device). Indeed when I query Firebase to get the User I obtain first the data from the cache and later a second change event with the latest data from the Firebase server, but it's too late!

Let's see the synchronous method 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;
}

I obtain the same behavior if I use addValueEventListener or addListenerForSingleValueEvent mixed with ref.keepSynced:

Let's say my user's score value in cache is 5 and from Firebase DB is 11.

When I call getUser I will obtain the score of 5 (Firebase ask cache first) so I will not call the doSomething() method.

If I uncomment the Thread.sleep() code from my example, the Firebase cache will have enough time to be updated and my getUser will return the correct score value (11).

So how can I directly ask the latest value directly from server side and bypass the cache?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
ThierryC
  • 1,794
  • 3
  • 19
  • 34
  • Also on: https://groups.google.com/forum/#!topic/firebase-talk/jFnO-QXKSwA – Frank van Puffelen Feb 17 '16 at 11:54
  • 1
    See http://stackoverflow.com/questions/34486417/firebase-offline-capabilities-and-addlistenerforsinglevalueevent, http://stackoverflow.com/questions/33260450/how-does-work-firebase-sync-with-share-data and https://groups.google.com/forum/#!msg/firebase-talk/ptTtEyBDKls/XbNKD_K8CQAJ – Frank van Puffelen Feb 17 '16 at 11:56
  • 1
    Unfortunately the topics you posted don't give a convenient answer. Even if I use the classic "addValueEventListener" I have the same problem that is on the "onDataChange(DataSnapshot data) { . . . }" event: **there is no way to know if the data came from the cache or from the network and thus if it's up-to-date**; so it's impossible to trust the data value in a synchronous treatment. – ThierryC Feb 17 '16 at 15:19
  • 2
    "it's impossible to trust the data value in a synchronous treatment" Yup. Since Firebase doesn't have "sync state", the best option is to not try that. Try to rephrase the use-case into "when the score is greater then 10, do abc". It may not be ideal, but it's the best approach at the moment. – Frank van Puffelen Feb 17 '16 at 22:25
  • 1
    @FrankvanPuffelen Why does firebase doesnot have a "sync state"? Is this a bug with respect to Firebase itself? When does the firebase check the delta and make changes for the server data to client side? Isn't there a mechanism for the user to force it? – Sreekanth Karumanaghat Sep 25 '17 at 14:52

8 Answers8

40

This was a problem that was causing me a lot of stress in my application too.

I tried everything, from changing .addListenerForSingleValueEvent() to .addValueEventListener() to trying to creatively use .keepSynced() to using a delay (the Thread.sleep() method you have described above) and nothing really worked consistently (even the Thread.sleep() method, which wasn't really acceptable in a production app didn't give me consistent results).

So what I did was this: after creating a Query object and calling .keepSynced() on it, I then proceed to write a mock/token object in the node I'm querying and THEN in that operation's completion listener, I do the data retrieval I want to do, after deleting the mock object.

Something like:

 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) {
                        }

                    });

                }

            });
}

This has worked consistently so far for me! (well, for a day or so, so take this with a grain of salt). It seems like doing a write operation before reading somehow bypasses the cache, which makes sense. So the data comes back fresh.

The only downside is the extra write operation before the read operation, which may cause a small delay (obviously use a small object) but if that's the price for always fresh data, I'll take it!

Hope this helps!

Vasily Kabunov
  • 6,511
  • 13
  • 49
  • 53
Antonis427
  • 1,012
  • 15
  • 22
  • That's sound a good workaround for lack of anything better. I cannot test that solution righ now but will try to, thanks...... – ThierryC Jul 01 '16 at 09:03
  • 3
    I really don't understand, why Firebase is architected so strangely. Is there no more way of doing it? :/ – Daniels Šatcs Jul 08 '16 at 15:29
  • just another question to Antonis427 : did you test what happen if you are momentarily (or completely )offline while your are saving the mock object? – ThierryC Nov 04 '16 at 11:13
  • Even if I validated the "runtransaction" response (which works well to refresh single node) this second answer works pretty well to query list of data (refreshing local persistence cache). The only point is to customize write rules to be able to write a mock on each node! And this method cannot be aplyed offline !! – ThierryC Jan 27 '17 at 11:01
  • 1
    Nice workaround. Did you guys come up with any other solution in the meantime? – steliosf Mar 17 '17 at 02:21
  • @Antonis427 How did you discover this work around? Just curious. :) – Sreekanth Karumanaghat Sep 27 '17 at 11:23
  • 1
    @HoangHuu I thought with the new database (firestore), you can query easy fresh one-time data. – J. Doe Oct 05 '17 at 22:50
  • 2
    _refreshMock_ do not have to be a child node of our node. It can be just any query, e.g. firebase.child("any_node").child("refreshMock") works as well – Hammer Jan 24 '18 at 21:40
15

A workaround I discovered is using Firebase's runTransaction() method. This appears to always retrieve data from the server.

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
            }
        }
    });
Robert
  • 1,090
  • 10
  • 13
  • interesting, I will need to test it (and also test the offline behaviour in that case ....) – ThierryC Nov 14 '16 at 09:37
  • 2
    This works but, if you're offline it won't complete until you're back online. Ended up wrapping it in a timeout and falling back to the addListenerForSingleValueEvent, and discarding the value when it's back online. – Carlos Fonseca Nov 22 '16 at 14:19
  • do you think I could also use this "runtransaction" method to query big list (instead of using ChildEventListener) ? – ThierryC Jan 26 '17 at 15:24
  • Finally I validated the previous response (sync by writing a MockObject) because using a transaction forces to give write access on any node you query, which not suit all model's rules; while a mock object can be written in dedicated writable node) – ThierryC Feb 14 '17 at 09:36
4

My Solution was to call Database.database().isPersistenceEnabled = true on load up, then call .keepSynced(true) on any nodes that I needed refreshed.

The issue here is that if you query your node right after .keepSynced(true) then you're likely to get the cache rather than the fresh data. Slightly lame but functional work around: delay your query of the node for a second or so in order to give Firebase some time to get the new data. You'll get the cache instead if the user is offline.

Oh, and if it's a node that you don't want to keep up to date in the background forever, remember to call .keepSynced(false) when you're done.

  • Some nodes can be synched all the time (current user ones), but the problem is "when to stop synching" in your solution. Marked answer as correct is good - you can do one simple operation (I use removeValue on non existent node) will synch you, in onComplete you can turn off synching as it got synched already. It is much simpler to make synching safe – Janusz Hain Aug 01 '17 at 07:41
  • Thanks for the feedback :) if it's true that using removeValue on a nonsense node forces a sync (I haven't tried it) then that seems like a good solution. Writing and then removing a value, as in the accepted answer, seemed a bit horrible to me. Truth be told all of this is a bit hack-y, and I was really surprised to learn that this is how Firebase sync behaves. – Julien Sanders Aug 03 '17 at 10:25
  • In my solutions removing non existent node worked. Yea, it is still a hack, I will need to think with my co-workers what to do, maybe we will turn off offline database, it turns to be really problematic sadly – Janusz Hain Aug 03 '17 at 13:00
  • I added answer with nice solution I guess (didn't test much, but can be interesting) – Janusz Hain Aug 11 '17 at 14:02
  • Cool! Looks interesting. Do you see any advantages to your new method over your original suggestion of using removeValue? – Julien Sanders Aug 12 '17 at 09:39
  • Yes, I tried removing value and it still does work only when firebase is online and I had to check wheter fb is online or not. If yes then remove value to sync and then do wanted query. So if we are online we need 3 calls to get what we want. It takes time to do it. I think adding value is better idea than removing as it will work offline too, so we need "only" 2 calls, but we are adding not needed value, so it is con too If we are offline we do 2 calls - to check if we are online and if not then get data from cache. There is no problem, fast calls. New sol. needs always just 1 call – Janusz Hain Aug 13 '17 at 05:49
3

Just add this code on onCreate method on your application class. (change the database reference)

Example:

public class MyApplication extendes Application{

 @Override
    public void onCreate() {
        super.onCreate();
            DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
            scoresRef.keepSynced(true);
      }
}

Works well to me.

Reference: https://firebase.google.com/docs/database/android/offline-capabilities

Cícero Moura
  • 2,027
  • 1
  • 24
  • 36
  • this works only to synchronyse the "score" node. While writing a mockObject allow to query "fresh" data on any node – ThierryC Jun 07 '17 at 12:40
  • Yes, but if you want to keep any node syncronized, simply remove Firebase.getDefaultConfig().setPersistenceEnabled(true); In my case, I wanted to keep only one node syncronized. – Cícero Moura Jun 19 '17 at 17:05
  • It is not good solution, because you listen then to EVERY data change on the node. If you got for example children nodes - each for other user then you will get large amount of data. One change in one user's data will trigger data synch for every other user that listens to this node. Instead of it it is better to turn on and off keepSynced only for really needed data. – Janusz Hain Aug 01 '17 at 07:37
2

I tried both accepted solution and I tried transaction. Transaction solution is cleaner and nicer, but success OnComplete is called only when db is online, so you can't load from cache. But you can abort transaction, then onComplete will be called when offline (with cached data) aswell.

I previously created function which worked only if database got connection lomng enough to do synch. I fixed issue by adding timeout. I will work on this and test if this works. Maybe in the future, when I get free time, I will create android lib and publish it, but by then it is the code 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<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)
            }
        }

If you want to make it faster for offline (to don't wait stupidly with timeout when obviously database is not connected) then check if database is connected before using function above:

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");
  }
});
Janusz Hain
  • 597
  • 5
  • 18
2

Use this line

FirebaseDatabase.getInstance().purgeOutstandingWrites();

  • For those wondering: works perfectly for canceling a transaction operation, returning a "DatabaseError: The write was canceled by the user" on onComplete callback. Thanks, you are a real life "salvador"! :D – Geraldo Neto Dec 19 '20 at 19:11
2

On Flutter, here's the way to force get latest snapshot from realtimeDb:

Future<DataSnapshot> _latestRtdbSnapshot(DatabaseReference dbRef) async {
  // ! **NASTY HACK**: Write/delete a child of the query to force an update.
  await dbRef.keepSynced(true);
  await dbRef.child('refreshMock').remove();
  final res = await dbRef.once();
  dbRef.keepSynced(false);
  return res;
}
jddymx
  • 41
  • 1
  • 1
  • 5
1

I know its late but for someone who is facing the same issue i finally found the perfect solution. You can check if the data is coming from the cached memory by using this method:

if (documentSnapShot.metadata.isFromCache){
   // data is coming from the cached memory
} else {
   // data is coming from the server
}

Here's a complete code snippet in Kotlin:

gameRoomRef.addSnapshotListener { documentSnapshot, fireStoreException ->

            fireStoreException?.let {

                return@addSnapshotListener
            }

            documentSnapshot?.let {

                if (it.metadata.isFromCache) 
                   // data is from cached memory
                else 
                   // data is from server
               
            }
Junior
  • 170
  • 2
  • 10