Wait for query inside for loop - Flutter Firebase - flutter

I have this function that creates a list of users from their ids. A query is made for each id to get the information from Firebase RTDB.
static List<FrediUser> formattedParticipantListFromIDs(List<String> participantIDs) {
List<FrediUser> formattedParticipants = [];
for (String id in participantIDs) {
dbQuery(2, 'users', id).once().then((value) {
formattedParticipants.add(FrediUser.fromMap(value.snapshot.value as Map));
});
}
print(formattedParticipants.length);
Future.delayed(const Duration(seconds: 3), () {
print(formattedParticipants.length);
});
return formattedParticipants; }
Output of 1st print statement
0
Output of 2nd print statement
3
The Problem
The return is always an empty list. As you can see, the loop and the return statement do not wait for the list to be populated. BUT, if you wait 3 seconds, the list did get populated, but it is too late.
How can I wait, in each iteration of the for loop, for the query to be done AND the .then to be called and completed?
*This method is called in a factory constructor function of a class, therefore futures and awaits cannot be used.

You'll want to use await to wait for the asynchronous once() method.
// 👇
static Future<List<FrediUser>> formattedParticipantListFromIDs(List<String> participantIDs)
async {
// 👆
List<FrediUser> formattedParticipants = [];
for (String id in participantIDs) {
// 👇
var value = await dbQuery(2, 'users', id).once();
formattedParticipants.add(FrediUser.fromMap(value.snapshot.value as Map));
}
print(formattedParticipants.length);
return formattedParticipants;
}
As you'll see, this means that your formattedParticipantListFromIDs function has to be marked as async, and returns a Future. There is no way to prevent this, as there's no way to make asynchronous code behave synchronously.
For more on this, I recommend taking the Dart codelab on Asynchronous programming: futures, async, await

Related

Flutter function returning at await statement

I am using flutter with the cbl package to persist data. Trying to retrieve the entries does not seem to work because the function created is returning at the await statement and not the return statement. This does not seem like the intended result of darts async/await functionality. So I am lost.
task_database.dart
Future<dynamic> getAllTasks() async {
final tasksDb = await Database.openAsync(database); <---------- Returns here
var tasksQuery = const QueryBuilder()
.select(SelectResult.all())
.from(DataSource.database(tasksDb));
final resultSet = await tasksQuery.execute();
late var task;
await for (final result in resultSet.asStream()) {
final map = result.toPlainMap();
final taskDao = TaskDao.fromJson(map);
task = taskDao.task;
// Do something with the task...
print(task);
}
;
return task; <-------------------------------------------- Does not make it here
}
task_cubit.dart
getAllTasks() => {
allTaskMap = TasksAbcDatabase().getAllTasks(),
emit(TaskState(tasks: state. Tasks))
};
What I have tried. I have tried to use Database.openSync instead of Database.openAsync however, the function just returns at the next await statement. I have also tried making getAllTasks asynchronous and awaiting the database as such.
Future<void> getAllTasks() async => {
allTaskMap = await TasksAbcDatabase().getAllTasks(),
emit(TaskState(tasks: state. Tasks))
};
However this has the same issue, when the function from task_database returns prematurely it the returns at the first await function in getAllTasks which is the allTaskMap variable.
Thanks
A function cannot "return prematurely" without a return statement.
The only way the execution is cut short would be an exception being thrown.
I also don't see how you don't get syntax errors, when you don't await the Database.openAsync(database) statement.
So make sure all your awaits are in place. Use the linter to find those that are missing. While you are at it, remove the keyword dynamic from your vocabulary, it will only hurt you if you use it without a need for it. Your return type should be properly typed, then your compiler could tell you, that returning a single task from a function that is clearly supposed to return multiple tasks is not going to work.
Either catch your exceptions and make sure you know there was one, or do not catch them and watch them go all the way through into your debugger.
In addition, following the comment of #jamesdlin, your function definitions are... valid, but probably not doing what you think they are doing.
Future<void> getAllTasks() async => {
allTaskMap = await TasksAbcDatabase().getAllTasks(),
emit(TaskState(tasks: state. Tasks))
};
needs to be
Future<void> getAllTasks() async {
allTaskMap = await TasksAbcDatabase().getAllTasks();
emit(TaskState(tasks: state. Tasks));
}

My firebase callable cloud function is called twice (when I called only once), how can I avoid my flutter app from failure?

I am trying to make a callable function in firebase, but when I call it from my flutter app it is called twice.
The Issue is, I am calling the function while sending some parameters to perform on by cloud function.
The automatic second call initiates with null parameter, which causes chaos because the function return empty list.
Here is my function code.
callable function
exports.getItem = functions.https.onCall(async (data, context) => {
// Data NULL Check
if (data == null) {
console.log("NULL DATA RECIEVED");
console.log(`null event id ${context.eventID}`);
} else {
console.log(`event id ${context.eventID}`);
const itemList = [];
console.log("Begin...............");
console.log(data);
console.log(data["closet"]);
console.log(data["from"].toString());
console.log(data["count"].toString());
const querySnapshot = await admin.firestore().collection("t_shirt").get();
querySnapshot.doc.forEach((doc)=>{
itemList.push(doc);
});
console.log(itemList.length);
if (itemList.length > 0) {
return itemList;
}
}
});
As I know I have to return something, because it's a promise.
So If I return empty list if the data is null, the app recieves empty list, while the first call is still fetching the data and making a list.
Okay the issue was it's querySnapshot.docs and not querySnapshot.doc

Rewrite sort((a, b) =>) to being async [duplicate]

I have this code :
widget.items.sort((a, b) {
await getItemDistance(a, true);
await getItemDistance(b, false);
return (itemADistance)
.compareTo(itemBDistance);
});
I am trying to sort widget.items list based on values returned from getItemDistance. However I get an error with a red squiggly line that says :
The await expression can only be used in an async function
and when I try to add async to the sort method I get another red squiggly line that says :
The argument type 'Future Function(Item, Item)' can't be assigned
to the parameter type 'int Function(Item, Item)'
How do I solve this dilemma guys ? :)
Thanks
List.sort is a synchronous function that expects a synchronous callback; there is no way to use it with an asynchronous callback. I would recommend transforming your List and doing any necessary asynchronous work first and then synchronously sorting the results. Doing so also should avoid calling getItemDistance (which, by virtue of being asynchronous, is likely to be expensive) on the same item multiple times. For example, something like:
final computedDistances = <Item, double>{};
for (final item in widget.items) {
final distance = await getItemDistance(item, ...);
computedDistances[item] = distance;
}
widget.items.sort((a, b) =>
computedDistances[a].compareTo(computedDistances[b])
);
(I don't know what the second argument to getItemDistance represents; if you can't get around that, you would need to build one Map with getItemDistance(..., true) results and one with getItemDistance(..., false) results1.)
As a last resort, you could write your own asynchronous sort function.
Edit #1
This should be a more efficient version since it doesn't wait for each asynchronous operation one-by-one:
final computedDistances = await Future.wait<double>([
for (final item in widget.items) getItemDistance(item, ...),
]);
final computedDistancesMap = <Item, double>{
for (var i = 0; i < widget.items.length; i += 1)
widget.items[i]: computedDistances[i],
};
widget.items.sort((a, b) =>
computedDistancesMap[a].compareTo(computedDistancesMap[b])
);
Edit #2
I've added a List.sortWithAsyncKey extension method to package:dartbag that can do this :
await widget.items.sortWithAsyncKey(
(element) => getItemDistance(element, ...),
);
1 However, if you need to call getItemDistance(..., true) for the first argument and getItemDistance(..., false) for the second argument, that implies that your comparison function probably is not self-consistent.
Whenever you are running async code you should add the async keyword like below. this tells dart you intend to run async code.
Async functions return a Future which tells dart that at some point in the future you are expecting itemADistance or an error.
You will want to return a Future of type int since you are expecting to receive an int itemADistance or an error.
Future<int> widget.items.sort((a, b) async {
await getItemDistance(a, true);
await getItemDistance(b, false);
return (itemADistance)
.compareTo(itemBDistance);
});

Cannot get subcollection along with collection in flutter

I am trying to get documents with their own subcollections, from Stream, but I am stuck.
This is where I set up my StreamSubscription:
Future<void> _toggleOrdersHistorySubscription({FirebaseUser user}) async {
_ordersHistorySubscription?.cancel();
if (user != null) {
_ordersHistorySubscription = ordersRepository
.ordersHistoryStream(userId: user.uid)
.listen((ordersSnapshot) {
final List<OrderModel> tmpList = ordersSnapshot.documents.map((e) {
// e.reference.collection("cart").getDocuments().;
return OrderModel.orderFromSnapshot(e);
}).toList();
add(OrdersHistoryUpdated(ordersHistory: tmpList));
});
}
}
The issue is that I can't see a way to get subcollection along with the parent document because getDocuments returns a Future.
Anyone can clear this issue for me?
So, I update the code method a separate method for retrieving data when listener is triggered but it doesn't work fully and I do not understand what's happening and why part of the code is working and part is not.
List<OrderModel> _getOrdersHistory({
#required QuerySnapshot snapshot,
}) {
return snapshot.documents.map((document) {
List<OrderedProductModel> cart = [];
List<AddressModel> addresses = [];
document.reference.collection("cart").getDocuments().then((snapshot) {
snapshot?.documents?.forEach((doc) {
cart.add(OrderedProductModel.fromSnapshot(doc));
});
});
document.reference
.collection("addresses")
.getDocuments()
.then((snapshot) {
snapshot?.documents?.forEach((doc) {
addresses.add(AddressModel.addressFromJson(doc.data));
});
});
final order = OrderModel.orderFromSnapshot(
document,
restaurantCart: cart,
);
return order.copyWith(
orderAddress:
(addresses?.isNotEmpty ?? false) ? addresses.first : null,
sentFromAddress:
(addresses?.isNotEmpty ?? false) ? addresses.last : null,
);
})
.toList() ??
[];
}
As an alternate solution to my original issue is that I made a map entry in Firestore instead of a collection for 2 address documents (which are set above as orderAddress and sentFromAddress) and for the cart I decided to get the data when needed for every cart item.
So the method which I put in the update is not the final one, but I want to understand what is happening up there as I do not understand why:
Why the cart is shown as empty if I do a print(order); right before the return and in the bloc it has data;
Why the orderAddress and sentFromAddress are both empty no matter what I try;
To be short: You'll never be able to get a List synchronously if you get the data async from firebase.
Both questions have the same answer:
Your timeline:
For each document
Create an empty list
Initiate the firebase query - getDocuments()
Subscribe to the returned future with - .then((snapshot){cart.add(...)}).
This lambda will be invoked when the documents arrived.
Another subscribe
Save your empty cart and first/last of empty addresses to an OrderModel
Your List contains the references to your empty lists indirectly
Use your bloc, some time elapsed
Firebase done, your callbacks starts to fill up your lists.
Regarding your comment like stream.listen doesn't like async callbacks:
That's not true, you just have to know how async functions work. They're run synchronously until the first await, then return with an incomplete future. If you do real async things you have to deal with the consequences of the time delay like changed environment or parallel running listeners.
You can deal with parallel running with await for (T v in stream) or you can use subscription.pause() and resume.
If anything returns a future, just do this:
...getDocuments().then((value) => {
value is the item return here. Do something with it....
})
Also, you might want to split your method up a bit to share the responsibility.
If getDocuments is a Future function and you need to wait for it, I think you should add await before it. I also don't see the snapshot status checking in your code pasted here. Maybe you have already checked the snapshot status in other function? Make sure the snapshot is ready when using it.

Flutter, getting database records and then internet json

I have a simple table from which I'm fetching a list of records. Once I get the records, then I have to get information online for each of the records. The code to do this is as follows:
class UserStationList {
List<UserStationListItem> _userStations = [];
final StreamController<HomeViewState> stateController;
UserStationList({#required this.stateController});
Future fetchUserStations() async {
stateController.add(HomeViewState.Busy);
//Fetch stations from table.
List<Map<String, dynamic>> stations =
await UserStationDatabase.instance.queryAllRows();
//If there are no stations, return and tell the screen to display the no data message.
if (stations.length == 0) {
stateController.add(HomeViewState.NoData);
return;
}
//Loop through each of the stations in the list and build the collection.
stations.forEach((station) async {
UserStationListItem newItem =
await _getPurpleAirSiteData(station['_id'], station['stationid']);
_userStations.add(newItem);
});
//When done, let the screen know.
stateController.add(HomeViewState.DataRetrieved);
}
Future<UserStationListItem> _getPurpleAirSiteData(
int id, int stationId) async {
var response = await http.get('$kURL$stationId');
var data = json.decode(response.body);
return UserStationListItem(
id: id, stationId: stationId, stationName: data['results'][0]['Label']);
}
}
The problem that I am running into involves the futures. I am processing the loop in a forEach and calling into the _getPurpleAirSiteData function for each. Within that function I have to await on the http.get to bring in the data. The stateController.add(HomeViewState.DataRetrieved) function is being called and the function exits long before the loop is completed. This is resulting in the data not being available when the StreamBuilder that I have receiving the data is run.
How can I set this up so that the loop runs completely before calling stateController.add?
I would change this part of code to a list of Futures and await-ing on it.
//Loop through each of the stations in the list and build the collection.
stations.forEach((station) async {
UserStationListItem newItem =
await _getPurpleAirSiteData(station['_id'], station['stationid']);
_userStations.add(newItem);
});
To:
List<Future<UserStationListItem>> listOfFutures = [];
stations.forEach((station) {
listOfFutures.add(_getPurpleAirSiteData(station['_id'], station['stationid']));
});
var stationItems = await Future.wait(listOfFutures);
stationItems.forEach((userStationListItem) {
_userStations.add(userStationListItem);
});
What I am essentially doing creating a list of Futures with your server request. Then await on it which returns a list of item result Maintaining index, which in turn ensures that requests are completed before you hit statecontroller.add. You also gain a performance gain since all request are not going one by one and instead asynchronously. Then you just iterate through the future result and add it to your item list.