14

I converted one of my apps to the new Firestore. I am doing things like saving a document on a button click, and then in the onSuccess listener, going to a different activity.

I also use the fact that Firestore save operations return tasks, to group tasks together using Tasks.whenAll:

val allTasks = Tasks.whenAll(
       createSupporter(supporter),,
       setStreetLookup(makeStreetKey(supporter.street_name)),
       updateCircleChartForUser(statusChange, createMode = true), 
       updateStatusCountForUser(statusChange))

      allTasks.addOnSuccessListener(this@SignUpActivity, successListener)
      allTasks.addOnFailureListener(this@SignUpActivity, onFailureListener)

Finally, I get the document id from a successful save and store it in preferences or in a local database for later use (within the onSuccessListener)

This all works great. Until there is a loss of network connectivity. Then everything falls apart, because the tasks never complete and the onSuccess/onFailure/onComplete listeners never get called. So the app just hangs.

I am working around this by checking for network availability before each save, and then doing a work-around by creating tasks without any listeners. I am also generating a document id locally using a UUID generator.

This, BTW, was not the way the app worked with the old firebase. In that case, everything ran nicely when offline and I saw documents getting synced up whenever the app came online.

My workaround for Firestore seems a terrible hack. Has anyone come up with a better solution?

See related Firestore database on insert/delete document callbacks not being invoked when there is no connection addOnCompleteListener not called offline with cloud firestore

Dutch Masters
  • 1,429
  • 17
  • 19
  • Have you tried timeouts? There are several ways of implementing them, simply perform a canceling action when some time (say 3-5 seconds) elapses. – Sergey Emeliyanov Dec 18 '17 at 13:41
  • The Task api doesn't directly let you cancel a task. So you'd have to do it some other way. – Dutch Masters Dec 19 '17 at 06:06
  • One thing to be aware of is that when you do an add/update with Firestore, it immediately creates a record in your local cache. Then it tries to sync with the server when it can. So it almost is like a 2-phase commit. But once the data is in the local cache, it won't be rolled back it seems. The listeners are waiting for the server sync, but it would be better if there was a way for a listener to return on local sync. – Dutch Masters Dec 19 '17 at 06:08
  • As Alex says in his answer "When there is a loss of network connectivity (there is no network connection on user device), neither onSuccess() nor onFailure() are triggered.". This has always been the behavior in the Firebase Realtime Database too: only writes that are committed on the server are considered failed/succeeded, before that they're pending. – Frank van Puffelen Dec 23 '17 at 18:45
  • 1
    @Frank Firebase doesn't have onSuccess()/onFailure() listeners at all. It is completely asynchronous, so while stuff may be happening when an app is going on/offline, I don't have to know about it. It just works. Firestore also claims to be asynchronous, but in reality the docs aren't clear about the differences. – Dutch Masters Dec 24 '17 at 18:24

5 Answers5

14

Cloud Firestore provide us feature for handle offline data but you need to use “Snapshot” (QuerySnapshot, DocumentSnapshot) to handle this case, unfortunately it not documented well. This is some code example (I use Kotlin Android) to handle case using Snapshot:

UPDATE DATA:

db.collection("members").document(id)
  .addSnapshotListener(object : EventListener<DocumentSnapshot> {
      override fun onEvent(snapshot: DocumentSnapshot?,
                           e: FirebaseFirestoreException?) {
          if (e != null) {
              Log.w(ContentValues.TAG, "Listen error", e)
              err_msg.text = e.message
              err_msg.visibility = View.VISIBLE;
              return
          }
          snapshot?.reference?.update(data)

      }
  })

ADD DATA:

db.collection("members").document()
 .addSnapshotListener(object : EventListener<DocumentSnapshot> {
     override fun onEvent(snapshot: DocumentSnapshot?,
                          e: FirebaseFirestoreException?) {
         if (e != null) {
             Log.w(ContentValues.TAG, "Listen error", e)
             err_msg.text = e.message
             err_msg.visibility = View.VISIBLE;
             return
         }
         snapshot?.reference?.set(data)

     }
 })

DELETE DATA:

db.collection("members").document(list_member[position].id)
   .addSnapshotListener(object : EventListener<DocumentSnapshot> {
       override fun onEvent(snapshot: DocumentSnapshot?,
                            e: FirebaseFirestoreException?) {
           if (e != null) {
               Log.w(ContentValues.TAG, "Listen error", e)
               return
           }
           snapshot?.reference?.delete()
       }
   })

You can see code example here: https://github.com/sabithuraira/KotlinFirestore and blog post http://blog.farifam.com/2017/11/28/android-kotlin-management-offline-firestore-data-automatically-sync-it/

Sabit Huraira
  • 307
  • 2
  • 5
  • Actually this method just basically ignores the future that is returned by the update() call. If you install a listener on the update() call (even within the snapshotlistener), you will still see that it is NOT CALLED. So functional wise this is exactly the same as just ignoring the future directly. And this method have the issue that the update will be called multiple times. – Phuah Yee Keat Sep 01 '18 at 01:40
2

When there is a loss of network connectivity (there is no network connection on user device), neither onSuccess() nor onFailure() are triggered. This behavior makes sense, since the task is considered completed only when the data has been committed (or rejected) on the Firebase server. So onSuccess() will fire only when the task completes successfully.

There is no need to check for network availability before each save. There is a workaround that easily can help you see if the Firestore client indeed can't connect to the Firebase server, which is by enabling debug logging:

FirebaseFirestore.setLoggingEnabled(true);

Operations that write data to the Firestore database are defined to signal completion once they've actually committed to the backend. As a result, this is working as intended: while offline they won't signal completion.

Note that the Firestore clients internally guarantee that you can read your own writes even if you don't wait for the completion of the task from delete. The Firestore client is designed to continue functioning fine without an internet connection. So writing/deleting to the database without an internet connection is (by design) possible, and will never yield an error.

Alex Mamo
  • 130,605
  • 17
  • 163
  • 193
  • I don't see how turning on logging is a "workaround". The basic question is how to make a firestore app work the same offline and online. Obviously I don't want users waiting around all day for listeners to fire if their device drops off the network. – Dutch Masters Dec 24 '17 at 18:30
  • You cannot do Firestore work when is online in the same way when is offline. That's why we have 2 different behaviours because there are two different states. While the device is offline, your listeners will receive listen events when the locally cached data changes. You can listen to documents, collections, and queries but those methods will be called only when you are back online and only when the data is committed or rejected by the server. – Alex Mamo Dec 25 '17 at 08:19
  • Furthermore, if you want to check whether you're receiving data from the server or the cache, use the `fromCache` property on the SnapshotMetadata in your snapshot event. If fromCache is `true`, the data came from the cache and might be incomplete. If `fromCache` is `false`, the data is complete and current with the latest updates on the server. – Alex Mamo Dec 25 '17 at 08:21
0

I found out how to do it using info at http://blog.farifam.com. Basically you must use SnapshotListeners instead of OnSuccess listeners for offline work. Also, you cannot use Google's tasks because they won't compete offline.

Instead (since Tasks are basically Promises), I used the Kotlin Kovenant library which can attach listeners to promises. One wrinke is that you must configure Kovenant to allow multiple resolution for a promise, since the event listener can be called twice (once when the data is added to the local cache, and once when it is synced to the server).

Here is an example snippet of code, with success/failure listeners, that runs both online and offline.

val deferred = deferred<DocumentSnapshot, Exception>() // create a deferred, which holds a promise
// add listeners
deferred.promise.success { Log.v(TAG, "Success! docid=" + it.id) }
deferred.promise.fail { Log.v(TAG, "Sorry, no workie.") }

val executor: Executor = Executors.newSingleThreadExecutor()
val docRef = FF.getInstance().collection("mydata").document("12345")
val data = mapOf("mykey" to "some string")

docRef.addSnapshotListener(executor, EventListener<DocumentSnapshot> { snap: DocumentSnapshot?, e: FirebaseFirestoreException? ->
    val result = if (e == null) Result.of(snap) else Result.error(e)
    result.failure {
        deferred.reject(it) // reject promise, will fire listener
    }
    result.success { snapshot ->
        snapshot.reference.set(data)
        deferred.resolve(snapshot) // resolve promise, will fire listener
    }
})
Dutch Masters
  • 1,429
  • 17
  • 19
0

For offline support you need to set Source.CACHE

docRef.get(Source.CACHE).addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
    @Override
    public void onComplete(@NonNull Task<DocumentSnapshot> task) {
        if (task.isSuccessful()) {
            // Document found in the offline cache
            DocumentSnapshot document = task.getResult();

        } else {
            //error
        }
    }
});
Pavel Poley
  • 5,307
  • 4
  • 35
  • 66
0

This code in Dart language since I use in Flutter but you easily can change it to your platform and language

Future<void> updateDoc(String docPath, Map<String, dynamic> doc) async {
    doc["updatedAt"] = Utils().getCurrentTimestamp();
    DocumentReference documentReference = _firestore.doc(docPath);

    Completer completer = Completer();
    StreamSubscription streamSubscription;
    streamSubscription = documentReference
        .snapshots(includeMetadataChanges: true)
        .listen((DocumentSnapshot updatedDoc) {
      // Since includeMetadataChanges is true this will stream new data as soon as
      // it update in local cache so it data has same updateAt it means it new data
      if (updatedDoc.data()["updatedAt"] == doc["updatedAt"]) {
        completer.complete();
        streamSubscription.cancel();
      }
    });
    documentReference.update(doc);
    return completer.future;
  }

So since includeMetadataChanges is set it will send data to stream when local cache changes to so when you call update you will receive data as soon as local cache update. You can use Completer to complete your future. Now your method only wait to update local cache and you can use await for updateDoc

mahdi shahbazi
  • 1,882
  • 10
  • 19