flutter firstWhere not responding (firestore) - firebase

I did a function a few months ago where my application is waiting for the user documents and responding accordingly. It was working like a charm until I optimized and updated the project to the last version.
If there is a user document, the stream yields the document and closes the stream.
If there is no user data in the cloud firestore, the stream yields null and awaits for the document to appear in the cloud.
// this function doesn't work properly and it should work but `firstWhere` is not
// listening to the stream unless there is a listener already which makes no sense
static Stream<DocumentSnapshot> get getUserDocumentWhenExists async* {
User user = FirebaseAuth.instance.currentUser;
if (user == null) throw 'No user is signed in';
FirebaseFirestore firebase = FirebaseFirestore.instance;
CollectionReference usersCollection = firebase.collection('users');
Stream<DocumentSnapshot> userDocumentStream = usersCollection.doc(user.uid).snapshots();
userDocumentStream.listen((event) {}); // <= this is here for the code to work
DocumentSnapshot userDocument = await userDocumentStream.first;
if (userDocument.exists == false) {
yield null;
yield await userDocumentStream.firstWhere((userDocument) {
// not beeing called without a previous listener
return userDocument.exists;
});
} else {
yield userDocument;
}
}
If you run this code without removing userDocumentStream.listen((event) {}) it will work without a problem as it did before the update.
My questions are:
Is this a bug?
Why is this happening? or did I just wrote something wrong?
Edit: I made a custom test without firebase and everything works fine. Just in this particular case firstWhere() is not listening to the stream
Edit2: after some more testing I discovered that any listener after userDocumentStream.first will not work. Now I'm more confused and I really need some help

I think after first() is called, the subscription is canceled even if in the first documentation it says otherwise:
"Internally the method cancels its subscription after the first element. This means that single-subscription (non-broadcast) streams are closed and cannot be reused after a call to this getter."
My solution:
Create a snapshots() stream for the first() and one for firstWhere()
final documentReference = usersCollection.doc(user.uid);
final userDocument = await documentReference.snapshots().first;
if (userDocument.exists == false) {
yield null;
yield await documentReference.snapshots().firstWhere((userDocument) {
return userDocument.exists;
});
} else {
yield userDocument;
}

Related

Why does persistenceEnabled: true result in my queries giving incorrect results?

I am using Firestore in my Flutter app. When I query a collection, I am getting the incorrect number of documents back.
The correct number of documents I should be getting back for my query is 20.
If I initialise Firebase as follows...
await Firebase.initializeApp();
FirebaseFirestore.instance.settings = Settings(persistenceEnabled: true);
I get only 2 documents back from my query.
If I initialize Firebase with peristenceEnabled false...
await Firebase.initializeApp();
FirebaseFirestore.instance.settings = Settings(persistenceEnabled: false);
I am wondering if it has to do with the fact I am only grabbing the first event in the stream. My query is as follows...
static Future<List<String>> myQuery(String personId , String bagId , String batchId , List<String> items) async {
var db = FirebaseFirestore.instance;
var q = db.collection('people')
.doc(personId)
.collection('bags')
.doc(bagId)
.collection('batches')
.where('batchId', isEqualTo: batchId)
.where('itemId', whereIn: items)
.where('status', isEqualTo: 'active');
var stream = q.snapshots().map((snapshot) {
List<String> results = [];
for (var doc in snapshot.docs) {
results.add(doc.id);
}
return results;
});
return stream.first;
}
}
If persistence is enabled this method returns a list of incorrect length. If persistence is disabled, this returns a list of the correct length.
I would expect the built in firestore caching mechanism would be smart enough to detect that any cached data is stale. I am therefore wondering if there is something wrong with my firestore data in general, such that it is breaking client side persistence/caching.
If you call snapshots() on a query, the Firestore SDK immediately invoked your callback with whatever data it has in the cache for that query (if any). It then checks with the server for any updates to the data, and (if any) invokes you callback again with the latest data.
But since you then call first() on the stream, you are only getting that first data from the local cache, and not the data from the server. If you only care about the current data, you should use get() instead of snapshots(), as that will first check for updates from the server before it invokes your callback.
So:
var snapshot = await q.get();
List<String> results = snapshot.map((doc) {
return doc.id;
});
return results;

checking if querysnapshot has made a connection to the firestore

When a certain document exists I want to go to another page then when a document does not exist. The code below does well when the app user has connection. However, when the connection to the internet is lost, the function will always return that it could not find the query. What I want to do is let the function give an error when it could not connect to the firestore so I can stay on the same page and try again.
Thank you in advance!
Future<bool> checkIfDocExists({String? id}) async {
var snapshot = await FirebaseFirestore.instance
.collection('rentals')
.where('DeviceID', isEqualTo: id)
.where('Status', isEqualTo: 0)
.get();
return (snapshot.docs.isNotEmpty);
}
In this case you should check device internet connection.
There is a package to help you to check internet status.
connectivity_plus => https://pub.dev/packages/connectivity_plus
connectivity => https://pub.dev/packages/connectivity
Code sample :
ConnectivityResult? _connectivityResult;
Future<void> _checkConnectivityState() async {
final ConnectivityResult result = await Connectivity().checkConnectivity();
if (result == ConnectivityResult.wifi) {
print('Connected to a Wi-Fi network');
} else if (result == ConnectivityResult.mobile) {
print('Connected to a mobile network');
} else {
print('Not connected to any network');
}
setState(() {
_connectivityResult = result;
});
}
You can check internet connection before read document from firestore.
If you want to make sure the document is read from the server, you can specify a source option for that:
...
.get(GetOptions(source: Source.server));
With this option the call will fail when it can't read the document from the server.
Also see the FlutterFire reference documentation for Source.

How to listen firebase updates in real time in Flutter

I have a stream which read user data from firebase. I trigger it after signin, it works perfectly. I show the data in my appbar. When I update firebase manually, I want to see the new date instantly on my appbar. First, I guessed there is something wrong with my appbar but then I noticed that my stream do not triggered when I update firebase data. Here is my stream code snippet. What am I missing?
static Future<User> userDataStream(userID) async {
print("userDataStream");
final databaseReference = Firestore.instance;
User currentUser = User();
await for (var snapshot in databaseReference
.collection('users')
.where('userID', isEqualTo: userID)
.snapshots()) {
currentUser.userName = snapshot.documents.first.data['userName'];
currentUser.email = snapshot.documents.first.data['email'];
currentUser.userID = snapshot.documents.first.data['userID'];
currentUser.level = snapshot.documents.first.data['level'];
currentUser.balance = snapshot.documents.first.data['balance'];
print(currentUser.balance);
return currentUser;
}
return currentUser;
}
How you are using this stream matters. await for starts listening to the user, then you do return currentUser; in it and break the await for. Therefore, it cannot keep listening to the stream in the future.
Instead of the return currentUser; inside await for, you can do something like setState((){this.renderedUser = currentUser;}) so that the user that comes from the server becomes the rendered one. If you do that, also add if (!mounted) return; inside the await for so that you stop listening to it when you realize you are in a different screen.
A better alternative may be to use the StreamBuilder widget.
If you run your current code, and make a change to the database, the print statement should be run again. That's because the snapshots is already listening for changes to the database, and calling your code when those happens.
The problem is that you return a Future<User> and a future can only be resolved (get a value) once. If you want to return live data that'd be a Stream<User> (and typically a StreamBuilder instead of a FutureBuilder to build a UI from it).

Issue adding element to list

I'm a total newbie to Flutter and I'm trying to add some data from Cloud Firestore to a list in Flutter, but having issues. I try to add the element, but after executing, the element isn't there. It's not throwing an exception or anything either. Maybe someone else has some advice for me!
I have tried changing the type of list (capture the doc from Cloud Firestore instead of data within the doc, same issue), I have also debugPrinted the data I am trying to store to make sure it exists, it does. I have done basic troubleshooting like running flutter clean as well. I am on the latest version of Flutter.
Firestore db = firestore();
List<String> getString() {
var dataList = new List<String>();
db.collection('Users').get().then((querySnapshot) {
querySnapshot.forEach((doc) {
dataList.add(doc.get('First name'));
});
});
debugPrint(dataList.first);
return dataList;
The list is empty, though it should contain the "First name" field on this Cloud Firestore doc. Again, verified the data does exist and prints when calling debugPrint.
The db.collection('Users').get() is a async function, so debugPrint(dataList.first); executes before of the end of your firestores get, because that your array returns empty.
If you try it:
db.collection('Users').get().then((querySnapshot) {
querySnapshot.forEach((doc) {
dataList.add(doc.get('First name'));
});
debugPrint(dataList.first);
});
You will see your data.
You can use await to wait the call finishes, so you must return a Future and use async key word on function declaration. This is a conceipt that you must know of flutter async functions (Async Flutter). So, the code below can solve your problem.
Firestore db = firestore();
Future <List<String>> getString() async {
var dataList = new List<String>();
var result = await db.collection('Users').get();
result.forEach((doc) {
dataList.add(doc.get('First name'));
});
debugPrint(dataList.first);
return dataList;
}

Firestore transaction fails with "Transaction failed all retries"

I'm running a very simple Firestore transaction which checks for the presence of a document, before writing to it if absent.
(The use case is registering a username - if it's not already registered, the current user gets to grab it)
Here's a snippet of the relevant Flutter code:
DocumentReference usernameDocRef =
Firestore.instance.collection(_USERNAMES).document(username);
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
});
This is failing with an exception "Transaction failed all retries".
Based on the Firestore documentation, failure can occur for two reasons:
The transaction contains read operations after write operations. Read operations must always come before any write operations.
The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.
I don't think I trigger either of those. Any suggestions?
The example transaction in the documentation uses await on its call to update. Perhaps you need the same on your call to set:
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
await transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
});
Firstly, try using the reference from the fresh snapshot and not from the original document reference. If this doesn't work, try changing [set] to [update] as I remember having the same error as you have experience now.
DocumentReference usernameDocRef =
Firestore.instance.collection(_USERNAMES).document(username);
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
await transaction.update(snapshot.reference, {
_UsernamesKey.userid: _user.id,
});
}
});
This has recently been fixed - https://github.com/flutter/plugins/pull/1206.
If you are using the master channel, the fix should be available already. For the other channels (dev, beta, stable) YMMV.
I'm not a Flutter/Dart expert, but I expect you have to return something from within the transaction, so that Firestore knows when you're done:
await Firestore.instance.runTransaction((transaction) async {
var snapshot = await transaction.get(usernameDocRef);
if (!snapshot.exists) {
return transaction.set(usernameDocRef, {
_UsernamesKey.userid: _user.id,
});
}
})

Resources