Lara
Lara

Reputation: 172

Flutter Firestore - Catching exceptions when listening to data streams

When fetching all documents of a collection from Firestore using the get() method, by default it won't throw an exception if there is no internet connection. Instead it looks up the cache and returns cached data. But sometimes you need to know whether a collection is actually currently empty or you just cannot connect to the database.

And for just fetching data there is a solution to that: the GetOptions parameter can be set to Source.server. That way an error will be thrown if the server cannot be reached.

Now I'm looking for an equivalent of this option for the snapshots() method that returns a Stream instead of just single data. So I need the stream to emit an error when it cannot connect to the server.

So:

await _firestore
        .collection('stuff')
        .where("someBool", isEqualTo: false)
        .get(const GetOptions(source: Source.server));

will throw a FirebaseException on bad connection, while:

_firestore
    .collection('stuff')
    .where("someBool", isEqualTo: false)
    .snapshots()
    .listen((snap) { ...  }, onError: (e) {
         //never called...
         debugPrint(e.toString());
     });

in this example I just can't find out.

Closest to a solution I got was checking on snap.metadata.isFromCache and throwing an exception when the value is true. But this does not work reliably. False meaning the snapshot is guaranteed to be up to date with the server. But in some cases it is true even though it got a valid response from the server.

Edit: isFromCache property would be a solution if I filtered someBool locally instead of directly in the query.

_firestore
    .collection('stuff')
    .where("someBool", isEqualTo: false)
    .snapshots(includeMetadataChanges: true)
    .listen((snap) { 
         //`hasPendingWrites` is false in all these cases:
         //  1. changing a document's `someBool` field from `false` to `true` and vise versa
         //  2. requesting with server connection
         //  3. requesting without server connection
         debugPrint("snap metadata pendingWrites is: ${snap.metadata.hasPendingWrites}");
         if (snap.metadata.isFromCache) {
         //I thought data from cache is only used when there's trouble connecting to the server which is not the case:
         //
         //no internet connection will indeed cause this value to be true
         //but changing a document's `someBool` field from `false` to `true` however, will  cause it to be true, too
             debugPrint("metadata of this snap is from cache -> throwing NoInternetException");
             throw NoInternetException();
         }
     }, onError: (e) {
         //never called...
         debugPrint(e.toString());
     });

Edit: I have offline persistence disabled:

db.settings = const Settings(persistenceEnabled: false);

Experiment: Updating a document's someBool field (directly from Firestore Console):

with internet:

true to false: pendingWrites is false, isFromCache is false

false to true: pendingWrites is false, isFromCache is true

When just turning the internet off while the Stream is being listened to:

pendingWrites is false, isFromCache is true

So the 'false to true' case with internet leads to a false positive using the isFromCache member.

Edit:

The documentation states:

If you called [DocumentReference.snapshots] or [Query.snapshots] with includeMetadataChanges parameter set to true you will receive another snapshot with isFomCache equal to false once the client has received up-to-date data from the backend.

Now in this false positive case (setting the bool from false to true) the Stream fires twice:

So for some reason Firestore reads from the cache even though there is a connection to the server. So I can't rely on this property when it comes to throwing NoInternetException.

Upvotes: 5

Views: 2057

Answers (4)

valley
valley

Reputation: 177

See my ticket i opened in June: https://github.com/firebase/flutterfire/issues/8961

As you can see in the last entry i've opened a feature request at Firebase support which asks for adding the GetOptions parameter in Query.snapshots and DocReference.snaphots calls.

Also you could have a look at the Nimbostratus package which tries to overcome the current limitations in Firestore.

Upvotes: 1

FDuhen
FDuhen

Reputation: 4806

The behavior you're describing is exactly what Firestore developers wanted, and indeed it has its drawbacks..

The only thing I'd add to your current solution would be a connectivity check using either something like connectivity_plus or internet_connection_checker

final _hasConnection = await yourConnectivityProvider.isConnected();

_firestore
    .collection('stuff')
    .where("someBool", isEqualTo: false)
    .snapshots(includeMetadataChanges: true)
    .listen((snap) { 
         //`hasPendingWrites` is false in all these cases:
         //  1. changing a document's `someBool` field from `false` to `true` and vise versa
         //  2. requesting with server connection
         //  3. requesting without server connection
         debugPrint("snap metadata pendingWrites is: ${snap.metadata.hasPendingWrites}");
         if (snap.metadata.isFromCache) {
         //I thought data from cache is only used when there's trouble connecting to the server which is not the case:
         //
         //no internet connection will indeed cause this value to be true
         //but changing a document's `someBool` field from `false` to `true` however, will  cause it to be true, too
             if (!_hasConnection) {
                 debugPrint("metadata of this snap is from cache -> throwing NoInternetException");
                 throw NoInternetException();
              }
         }
     }, onError: (e) {
         //never called...
         debugPrint(e.toString());
     });

connectivity_plus will only detect if the device has it's Wifi/4G ON, but it's pretty light and won't affect the battery.
internet_connection_checker will give you a 'real' info about the connectivity, but it has to query a server to get this info.

Will it work 100% of the time ? Probably not.
There are a few use-cases where, even with this 'security', you may get false positives (or even false-negatives, which is even worst).

Upvotes: 1

Marcelo Abu
Marcelo Abu

Reputation: 97

In order to do what you want you have to: First make sure that cache is disabled and cleared:

FirebaseFirestore.instance.settings =
    Settings(persistenceEnabled: false);

await FirebaseFirestore.instance.clearPersistence();

Now, when you try to access anything without network connection, you should get an empty snapshot with "isFromCache" equal to true. Unfortunately there is no error in this case. It is just the way it is designed.

Hope I helped.

Upvotes: 2

Shirsh Shukla
Shirsh Shukla

Reputation: 5973

Firebase applications work even if your app temporarily loses its network connection. In addition, Firebase provides tools for persisting data locally, managing presence, and handling latency.

like as example you can check the internet connection like that, Detecting Connection State

final connectedRef = FirebaseDatabase.instance.ref(".info/connected");
connectedRef.onValue.listen((event) {
  final connected = event.snapshot.value as bool? ?? false;
  if (connected) {
    debugPrint("Connected.");
  } else {
    debugPrint("Not connected.");
  }
});

and also for more details, you can check out this link:- https://firebase.google.com/docs/database/flutter/offline-capabilities

and for particularly firestore you can follow this,

db
    .collection('stuff')
    .where("someBool", isEqualTo: false)
    .snapshots(includeMetadataChanges: true)
    .listen((querySnapshot) {
  for (var change in querySnapshot.docChanges) {
    if (change.type == DocumentChangeType.added) {
      final source =
          (querySnapshot.metadata.isFromCache) ? "local cache" : "server";

      print("Data fetched from $source}");
    }
  }
});

for more detail follow this link(for firestore):- https://firebase.google.com/docs/firestore/manage-data/enable-offline#dart_2

Upvotes: 4

Related Questions