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;
}
Related
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
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,
});
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 have two methods the first one for getting a map stored in Firestore
Future daysMap(FirebaseUser user, String packageCode, int totalDays) async {
await users.document(user.uid).collection('myPackages')
.document(packageCode)
.get().then((doc){
// print(doc.data['DaysMap']);
return doc.data['DaysMap'];
});
}
It works correctly and prints out the map. The second method is for setting the map from Firestore to a new map in order to loop on it.
currentWorkout(FirebaseUser user, String packageCode,totalDays) async {
Map<dynamic, dynamic> days = await daysMap(user, packageCode, totalDays);
print(days);
}
When i print our the new map 'days' it always prints null.
Try using the async/await style.
Future daysMap(FirebaseUser user, String packageCode, int totalDays) async {
DocumentSnapshot doc = await users.document(user.uid).collection('myPackages').document(packageCode).get();
return doc.data['DaysMap'];
}
I try to combine flutter with the sembast nosql database. I created a method to get all db documents and edit the given data.
My method looks like this:
Future<List<Minute>> getAll() async {
final finder = Finder(sortOrders: [SortOrder('timestamp')]);
final recordSnapshots = await store.find(await _db, finder: finder);
return recordSnapshots.map((snapshot) {
final minute = Minute.fromMap(snapshot.value);
minute.id = snapshot.key;
return minute;
}).toList();
}
get getSum {
getAll().then((value) {
int total = value.fold(
0, (previousValue, element) => previousValue + element.value);
print(total); // The correct value
return total; // Here I dont retrieve the value
});
}
print(total) shows me the right value, but in the Widget I don´t retrieve it. I retrieve just null. Whats the issue here?
I think you should not return a value inside the then(...) maybe return just the future and use a FutureBuilder as widget
You should use pedantic/strong mode as it should show lint warnings (missing return type and missing return value) that could help solving your issue without running it.
Unoptimized solution based on your code:
Future<int> getSum() async {
var minutes = await getAll();
var total = minutes.fold<int>(
0, (previousValue, element) => previousValue + element.value);
return total;
}
Possible optimizations:
avoid the sort order when computing the sum
only read the value field instead of converting the whole object
Example:
Future<int> getSum() async {
var total = 0;
(await store.find(await _db)).forEach((snapshot) {
total += snapshot['value'] as int;
});
return total;
}
Unfortunately the return value cannot be a String, it could be a Future<String> though (i.e. db calls are async).
as Julian2611 noted you then need to use a FutureBuilder and for example .toString() to convert the value to a string.