Why is 'return' not awaiting this nested 'then' in Flutter/Dart? - flutter

I'm new to programming, and this is my first post on stack overflow! In building my first Flutter app I'm trying to understand the code, and I'm not sure why two pieces of code behave differently. Feel free to just look at the code and answer why one doesn't work... or if you'd like, here's the background.
Data structure:
Collection: chatters
Document: chatter doc
SubCollection: members-chatter, another SubCollection: approved-chatter
Documents: member doc, and approved doc
I'm listing all the chatter docs that a user is a member of, so from a CollectionGroup query with uid in the member doc, I then lookup the parent doc id. Next I want to have chatter docs be marked bool public, and for !public chatters, I only want them listed if the user's uid is also on an approved doc in SubCol approved-chatter.
So my main question is why the await doesn't hold through the entirety of the nested .then's in my first attempt.
But while I'm here, I'm also open to any insight on my approach to handling membership to groups of both public and approval-required types. It seems trickier than I first thought, once considering read/write permissions and appropriate security and privacy.
I tried this first.
// get my chatter IDs
Future<List<String>> oldgetMyChatterIDs() async {
List<String> myChatterIDs = [];
await FirebaseFirestore.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get()
.then(
(snapshot) => snapshot.docs.forEach((document) {
document.reference.parent.parent!.get().then((value) {
// the above 'then' isn't awaited.
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
// 'myChatterIDs' is returned empty before the above line fills the list.
} else {
// check if user is approved.
}
});
}),
);
// // adding the below forced delay makes the code work... but why aren't the 'thens' above working to holdup the return?
// await Future.delayed(const Duration(seconds: 1));
return myChatterIDs;
}
but return myChatterIDs; completes before:
document.reference.parent.parent!.get().then((value) {
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
}
Why doesn't the return await the await?
I rewrote the code, and this way works, but I'm not sure why it's different. It does appear a bit easier to follow, so I perhaps it's better this way anyway.
// get my chatter IDs
Future<List<String>> getMyChatterIDs() async {
List<String> myChatterIDs = [];
QuerySnapshot<Map<String, dynamic>> joinedChattersSnapshot = await FirebaseFirestore
.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get();
for (var i = 0; i < joinedChattersSnapshot.docs.length; i++) {
DocumentSnapshot<Map<String, dynamic>> aChatDoc =
await joinedChattersSnapshot.docs[i].reference.parent.parent!.get();
bool isPublic = aChatDoc.data()?['public'] ?? true;
if (isPublic) {
myChatterIDs.add(aChatDoc.id);
} else {
try {
DocumentSnapshot<Map<String, dynamic>> anApprovalDoc =
await FirebaseFirestore.instance
.collection('chatters')
.doc(aChatDoc.id)
.collection('approved-chatter')
.doc(FirebaseAuth.instance.currentUser!.uid)
.get();
bool isApproved = anApprovalDoc.data()!['approved'];
if (isApproved) {
myChatterIDs.add(aChatDoc.id);
}
} catch (e) {
// // Could add pending to another list such as
// myPendingChatterIDs.add(aChatDoc.id);
}
}
}
return myChatterIDs;
}

Take a look at this piece of code:
print("1");
print("2");
result:
1
2
The print on both lines is synchronous, they don't wait for anything, so they will execute immediately one after the other, right ?
Take a look at this now:
print("1");
await Future.delayed(Duration(seconds: 2));
print("2");
result:
1
2
This now will print 1, then wait 2 seconds, then print 2 on the debug console, using the await in this case will stop the code on that line until it finishes ( until the 2 seconds pass).
Now take a look at this piece of code:
print("1");
Future.delayed(Duration(seconds: 2)).then((_) {print("2");});
print("3");
result:
1
3
2
this seems to be using the Future, but it will not wait, because we set the code synchronously, so 1 will be printed, it will go to the next line, it will run the Future.delayed but it will not wait for it across the global code, so it will go to the next line and print 3 immediately, when the previous Future.delayed finishes, then it will run the code inside the then block, so it prints the 2 at the end.
in your piece of code
await FirebaseFirestore.instance
.collectionGroup('members-chatter')
.where('uid', isEqualTo: FirebaseAuth.instance.currentUser?.uid)
.where('status', isEqualTo: 'joined')
.orderBy('timeLastActive', descending: true)
.get()
.then(
(snapshot) => snapshot.docs.forEach((document) {
document.reference.parent.parent!.get().then((value) {
// the above 'then' isn't awaited.
if (value.data()?['public'] ?? true) {
myChatterIDs.add(document.reference.parent.parent!.id);
// 'myChatterIDs' is returned empty before the above line fills the list.
} else {
// check if user is approved.
}
});
}),
);
// // adding the below forced delay makes the code work... but why aren't the 'thens' above working to holdup the return?
// await Future.delayed(const Duration(seconds: 1));
return myChatterIDs;
using then, the return myChatterIDs will execute immediately after the Future before it, which will cause an immediate end of the function.
To fix this with the then, you need simply to move that return myChatterIDs inside the then code block like this:
/*...*/.then(() {
// ...
return myChatterIDs
});
using the await keyword in your second example will pause the code on that line until the Future is done, then it will continue for the others.
you can visualize what's happening live on your code, by setting breakpoints on your method lines from your IDE, then see what's running first, and what's running late.

It´s because document.reference.parent.parent!.get() is a Future too but you aren't telling your function to wait for it. The first await only applies to the first Future.
When you use then you are basically saying, execute this Future and when it's finished, do what is inside this function but everything outside that function doesn't wait for the then to finish unless you told it to with an await.
For example this code:
String example = 'Number';
await Future.delayed(const Duration(seconds: 1)).then(
(value) {
//Here it doesn't have an await so it will execute
//asynchronously and just continues to next line
//while the Future executes in the background.
Future.delayed(const Duration(seconds: 1)).then(
(value) {
example = 'Letter';
},
);
},
);
print(example);
Will result in Number being printed after 1 second. But this code:
String example = 'Number';
await Future.delayed(const Duration(seconds: 1)).then(
(value) async {
//Here it does have an await so it will wait for this
//future to complete too.
await Future.delayed(const Duration(seconds: 1)).then(
(value) {
example = 'Letter';
},
);
},
);
print(example);
Will result in Letter being printed after 2 seconds.
Additional to this, forEach cannot contain awaits as mentioned here in the Dart documentation and even if you use it will still be asynchronous.
Try using
for(var document in snapshot.docs){
//Your code here
}
Instead

Related

Collecting string from Firebase Firestore in flutter

I am creating like system and i want to get likeCount from firebase which i created.
It's collecting it but returns null,
here is my code:
String? getLikecount(tresc) {
String? likeCount;
FirebaseFirestore.instance
.collection('Posty')
.where('Tresc', isEqualTo: tresc)
.get()
.then((value) => value.docs.forEach((element) async {
var id = element.id;
final value = await FirebaseFirestore.instance.collection('Posty').doc(id).get();
likeCount = value.data()!['likeCount'].toString();
print(likeCount);
}));
print(likeCount);
return likeCount;
}
and here is console output:
Data is loaded from Firestore (and most modern cloud APIs) asynchronously, because it may needs to come from the network and we can't block your code (and your users) while waiting for it.
If we change the print statements a bit, and format the code, it'll be much easier to see what's going on:
String? getLikecount(tresc) {
String? likeCount;
FirebaseFirestore.instance
.collection('Posty')
.where('Tresc', isEqualTo: tresc)
.get()
.then((value) => value.docs.forEach((element) async {
var id = element.id;
final value = await FirebaseFirestore.instance
.collection('Posty')
.doc(id)
.get();
likeCount = value.data()!['likeCount'].toString();
print('In then: $likeCount');
}));
print('After then: $likeCount');
return likeCount;
}
If you run this, you'll see it outputs:
After then: null
In then: 0
This is probably not what you expected, but it explains perfectly why you don't get a result. By the time your return likeCount runs, the likeCount = value.data()!['likeCount'].toString() hasn't executed yet.
The solution is always the same: any code that needs the data from the database has to be inside the then handler, be called from there, or be otherwise synchronized.
In Flutter it is most common to use async and await for this. The key thing to realize is that you can't return something now that hasn't been loaded yet. With async/await you function becomes:
Future<String?> getLikecount(tresc) {
String? likeCount;
var value = await FirebaseFirestore.instance
.collection('Posty')
.where('Tresc', isEqualTo: tresc)
.get();
for (var doc in value.docs) {
var id = element.id;
final value = await FirebaseFirestore.instance
.collection('Posty')
.doc(id)
.get();
likeCount = value.data()!['likeCount'].toString();
print('In then: $likeCount');
}));
print('After then: $likeCount');
return likeCount;
}
Now your code returns a Future<String?> so a value that at some point will hold the string. When calling getLikecount you will now need to use then or await to handle the Future, and if you want to show the count in the UI you will have to store it in the State of a StatefulWidget.

Unexpected text "return" inside function

int FriendsNum() {
_firestore.collection('Friends').doc(User.userID).collection("Friends").where("Status",isEqualTo: 2)
.get()
.then((res)=> return{res.size});
return 0;
}
I am basically trying to get the number of documents that are inside the collection that has the status of 2, and the value should be returned so it can be displayed for the user, what can be changed in the syntax to make this work? with many thanks!
You have to change your function return type to a Future and use the await keyword to get the result from the firestore collection as it is an asynchronous operation itself.
The updated code will be the following:
Future<int> FriendsNum() async {
final count = await _firestore
.collection('Friends')
.doc(User.userID)
.collection("Friends")
.where("Status",isEqualTo: 2)
.get()
.then((res) => res.size);
return count;
}

Flutter pagination with firestore stream

How to properly implement pagination with firestore stream on flutter (in this case flutter web) ?
my current approach with bloc which is most likely wrong is like this
function called on bloc when load next page, notice that i increased the lastPage variable of the state by 1 each time the function is called:
Stream<JobPostingState> _loadNextPage() async* {
yield state.copyWith(isLoading: true);
try {
service
.getAllDataByClassPage(state.lastPage+1)
.listen((List<Future<DataJob>> listDataJob) async {
List<DataJob?> listData = [];
await Future.forEach(listDataJob, (dynamic element) async {
DataJob data= await element;
listData.add(data);
});
bool isHasMoreData = state.listJobPostBlock.length!=listData.length;
//Update data on state here
});
} on Exception catch (e, s) {
yield StateFailure(error: e.toString());
}}
function called to get the stream data
Stream<List<Future<DataJob>>> getAllDataByClassPage(
String className, int page) {
Stream<QuerySnapshot> stream;
if (className.isNotEmpty)
stream = collection
.orderBy('timestamp', "desc")
.where('class', "==", className).limit(page*20)
.onSnapshot;
else
stream = collection.onSnapshot;
return stream.map((QuerySnapshot query) {
return query.docs.map((e) async {
return DataJob.fromMap(e.data());
}).toList();
});
}
With this approach it works as intended where the data loaded increased when i load next page and still listening to the stream, but i dont know if this is proper approach since it replace the stream could it possibly read the data twice and end up making my read count on firestore much more than without using pagination. Any advice is really appreciated, thanks.
Your approach is not very the best possible indeed, and as you scale you going to be more costly. What I would do in your shoes would be to create a global variable that represents your stream so you can manipulate it. I can't see all of your code so I am going to be as generic as possible so you can apply this to your code.
First let's declare the stream controller as a global variable that can hold the value of your stream:
StreamController<List<DocumentSnapshot>> streamController =
StreamController<List<DocumentSnapshot>>();
After that we need to change your getAllDataByClassPage function to the following:
async getAllDataByClassPage(String className) {
Stream stream = streamController.stream;
//taking out of the code your className logic
...
if(stream.isEmpty){
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.limit(20)
.onSnapshot
streamController.add(snap.docs);
}else{
DocumentSnapshot lastDoc = stream.last;
QuerySnapshot snap = await collection.orderBy('timestamp', "desc")
.where('class', "==", className)
.startAfterDocument(lastDoc)
.limit(20)
.onSnapshot;
streamController.add(snap.docs);
}
}
After that all you need to do in order to get the stream is invoke streamController.stream;
NOTE: I did not test this code but this is the general ideal of what you should try to do.
You can keep track of last document and if has more data on the list using startAfterDocument method. something like this
final data = await db
.collection(collection)
.where(field, arrayContains: value)
.limit(limit)
.startAfterDocument(lastDoc)
.get()
.then((snapshots) => {
'lastDoc': snapshots.docs[snapshots.size - 1],
'docs': snapshots.docs.map((e) => e.data()).toList(),
'hasMore': snapshots.docs.length == limit,
});

How to return a List, after a Method fills it, Flutter

I'm stuck with a problem and I wondered if you can help me.
I have a functions (in Flutter) that returns a List of Items. Now this List of Items should be Filled by an other function, which goes thought my Database and collect the right items. My Problem is, that my Function runs after the Return Statement... Here is some Code:
Future<List<MaterialItem>> getItems(String path, String fach) async {
// This is a empty List that I want to fill
List<MaterialItem> list = [];
// That's my Function, that fills the List
var result = await _db
.collection("$path/$fach/")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
// Here the List gets filled
list.add(MaterialItem.fromSnapshot(doc.data() as Map<String, dynamic>));
});
});
// Here the List should be returned, but after my Function fills it.
return list;
}
Hope you know what my problem is, and someone can help me.
I think you could solve this using a Completer. Your function should return the Future property of the Completer and the database call should then complete it.
Take a look at the API and the example:
https://api.dart.dev/stable/2.12.4/dart-async/Completer-class.html
For example: (pseudo code)
Future<List<MaterialItem>> getItems(String path, String fach) async {
// declare a completer
Completer<List<MaterialItem>> completer = Completer();
List<MaterialItem> list = [];
final result = await _db
.collection("$path/$fach/")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
list.add(MaterialItem.fromSnapshot(doc.data() as Map<String, dynamic>));
});
// let the database call complete the completer
completer.complete(list);
});
// return the future value of the completer
return completer.future;
}

I need some guidance in the Future Asynchronous Calls with flutter and dart, sometimes things happen out of order

The following code works fine, because it return only a simple list, but in some cases that I need to do nested Firebase calls, I can't make things happen in the right order, and the main return statement comes incomplete. What can I do to improve my Future Asynchronous Calls?
Future<List<MyNotification>> getNotifications() async {
var uid = await FirebaseAuth.instance.currentUser();
List<MyNotification> tempNots = await Firestore.instance
.collection("notifications")
.where("targetUsers", arrayContains: uid.uid)
.getDocuments()
.then((x) {
List<MyNotification> tempTempNots = [];
if (x.documents.isNotEmpty) {
for (var not in x.documents) {
tempTempNots.add(MyNotification.fromMap(not));
}
}
return tempTempNots = [];
});
return tempNots;
}
The most important thing; don't use then inside your async functions. I modified your code like this;
Future<List<MyNotification>> getNotifications() async {
// Using the type definition is better.
FirebaseUser user = await FirebaseAuth.instance.currentUser();
// The return type of getDocuments is a QuerySnapshot
QuerySnapshot querySnapshot = await Firestore.instance
.collection("notifications")
.where("targetUsers", arrayContains: user.uid)
.getDocuments();
List<MyNotification> tempTempNots = [];
if (querySnapshot.documents.isNotEmpty) {
for (DocumentSnapshot not in querySnapshot.documents) {
tempTempNots.add(MyNotification.fromMap(not));
}
}
return tempTempNots;
}