How make async api calls inside loop and complete loop with all data from api call. Due to async I'm losing that part of data - flutter

I'm reading json List from device memory and want to perform some operations on it's components.
When I load that list I start loop where I check each item of that list.
While in loop I add each item to new List to have updated List after loop ends so I could save it on device memory.
If some conditions are true then I use future async http call to get updated data
then theoretically I update that item of the List while staying inside loop. And thus after loop ends I must have updated Json List ready to be saved on device memory.
Problem is that While I http call inside loop, the answer delays, loop ends and new Json List is being constructed and saved on memory without the component that was supposed to be updated.
Is there any way to force wait the whole loop or something else ?
Here is the code
Future<void> readStoredData() async {
try {
final prefs = await SharedPreferences.getInstance();
_rawJsonListE = prefs.getStringList('storedData');
List<String> rawJsonListNEW = [];
bool _isNeedUpdate = false;
_rawJsonListE!.forEach((item) async {
if (someCondition with item Data) {
_isNeedUpdate = true;
await makeHttpCallFutureAwaitFunction(item).then((_) {
rawJsonListNEW.add(updatedItem);
});
} else {
rawJsonListNEW.add(item);
}
});
if (_isNeedUpdate) prefs.setStringList('storedData', rawJsonListNEW);
}
notifyListeners();
} catch (error) {
print('Error : ${error}');
throw error;
}

You can separate the refreshing data part to another function.
// Just need to check _rawJsonListE is empty or not
_isNeedUpdate = _rawJsonListE.isNotEmpty();
Create a new function.
Future<List<String>> checkDataAndRefresh(List<String> _rawJsonListE) async {
List<String> rawJsonListNEW = [];
_rawJsonListE!.forEach((item) async {
if (someCondition with item Data) {
final String newString = await makeHttpCallFutureAwaitFunction(item);
rawJsonListNEW.add(newString);
} else {
rawJsonListNEW.add(item);
}
});
return rawJsonListNEW;
}
And if _isNeedUpdate is true, do work.
if (_isNeedUpdate)
final List<String> newData = await checkDataAndRefresh(_rawJsonListE);
prefs.setStringList('storedData', newData);

Related

Flutter setState not updating string variable

I am adding a file to firebase storage. After obtaining the firebase URL of the image, I use setState to set it equal to another string( fileUrl) and then add it to the Firestore database. The problem is it's not updating to the firebase URL and the String fileUrl still has its initialized value. This is the way I use to add the file and add it to the Firestore database, I'm not sure how to fix it.
First I select a file:
...
String fileUrl = 'temp';
...
Future uploadFile() async {
if (file == null) return Container();
final fileName = basename(file!.path);
final destination = 'files/$fileName';
task = FirebaseApi.uploadFile(destination, file!);
if (task == null) return;
final shot = await task!.whenComplete(() {});
final urlDownload = await shot.ref.getDownloadURL();
print('Download-Link: $urlDownload'); // This prints out the correct url
setState(() {
fileUrl = urlDownload.toString();
});
}
Then I upload to firebase database :
void postToFirebase(school, String fileName) {
print('fileUrl') ; //But when i check it here it still prints out 'temp'
FirebaseFirestore.instance
.collection("collection_name")
.doc(doc_name)
.collection("collection_name")
.add({
"attachmentUrl": fileUrl,
"attachment_name": fileName,
})
Then my Button (on pressed )
onPressed: () async {
uploadFile();
postToFirebase(school, fileName);
}
Not sure how to fix it. Any help will be much appreciated, thank you
The fileUrl field was updated in uploadFile method in an async way. So you are not guaranteed the field will be updated before the following method is called, which is the postToFirebase.
If you want to make it work sequentially, you can use promise/future to tune the process. New a promise, and complete it in the setState callback, and thus make uploadFile method return a future depended on the completion of the promise in setState callback method. And then chain the postToFirebase with the former one using future's then API.
Code example here:
String fileUrl = "";
Future updateFile() {
Completer completer = Completer();
setState(() {
fileUrl = "new";
completer.complete();
});
return completer.future;
}
void postToFireBase() {
// use the fileUrl updated here
}
void text() {
updateFile().then((value) => postToFireBase());
}

Flutter check if stream is empty before close end

I'm using BehaviorSubject as a Stream controller.
In one of my functions, I want to .add more items only in case the Stream is empty of events.
#override
Future<void> fetchNextOverviewPolls() async {
if (await _pollOverviewStreamController.isEmpty) return; // My Problem
final lastDoc = await _pollOverviewStreamController.last;
final querySnapshot =
await _overviewPollsRef.startAfterDocument(lastDoc).limit(5).get();
for (final doc in querySnapshot.docs) {
_pollOverviewStreamController.add(doc);
}
}
The isEmpty property returns a value in case the Stream ends. I want to check it when the Stream is still running.
How do I do that?
BehaviorSubject supports hasValue.
In the above case, use this line instead:
if (_pollOverviewStreamController.hasValue) return;

Does compute support asnc request?

I have a list of String address like:
List<String> addressStrings = [....];
I am using geocoding plugin to get the address data and marker for these address strings like:
//This is a class-level function
Future<List<MarkerData>> getMarkerDataList() async {
List<MarkerData> list = [];
addressStrings.forEach((element) async {
final result = await locationFromAddress(element);
final markerData = MarkerData(element, result.first);
list.add(markerData);
});
return list;
}
But it freezes the UI as expected. I tried to use compute to perform the operation in another isolate like:
//This is a top-level function
Future<List<MarkerData>> getMarkerDataList(List<String> addressStrings) async {
List<MarkerData> list = [];
addressStrings.forEach((element) async {
final result = await locationFromAddress(element);
final markerData = MarkerData(element, result.first);
list.add(markerData);
});
return list;
}
//This is a class-level function
Future<List<MarkerData>> getMarkerData()async{
final result = await compute(getMarkerDataList, addressStrings);
return result;
}
But it doesn't work and shows Unhandled exception in the console.
I guess final result = await locationFromAddress(element); request is the problem here. Because it do pass before that statement but doesn't this one.
So, my question is: does compute support async? If yes, what I am doing wrong here? If no, how can I do asynchronous performance intensive tasks like this efficiently without blocking the UI?
Yes, as far as I know async does support compute - here's an article that should help out:
https://medium.com/flutterdevs/flutter-performance-optimization-17c99bb31553

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,
});

Is there a way to get notified when a dart stream gets its first result?

I currently have an async function that does the following:
Initializes the stream
Call stream.listen() and provide a function to listen to the stream.
await for the stream to get its first result.
The following is some pseudo code of my function:
Future<void> initStream() async {
// initialize stream
var stream = getStream();
// listen
stream.listen((result) {
// do some stuff here
});
// await until first result
await stream.first; // gives warning
}
Unfortunately it seems that calling stream.first counts as listening to the stream, and streams are not allowed to be listened by multiple...listeners?
I tried a different approach by using await Future.doWhile()
Something like the following:
bool gotFirstResult = false;
Future<void> initStream() async {
var stream = getStream();
stream.listen((result) {
// do some stuff here
gotFirstResult = true;
});
await Future.doWhile(() => !gotFirstResult);
}
This didn't work for me, and I still don't know why. Future.doWhile() was successfully called, but then the function provided to stream.listen() was never called in this case.
Is there a way to wait for the first result of a stream?
(I'm sorry if I didn't describe my question well enough. I'll definitely add other details if needed.)
Thanks in advance!
One way is converting your stream to broadcast one:
var stream = getStream().asBroadcastStream();
stream.listen((result) {
// do some stuff here
});
await stream.first;
Another way, without creating new stream, is to use Completer. It allows you to return a Future which you can complete (send value) later. Caller will be able to await this Future as usual.
Simple example:
Future<int> getValueAsync() {
var completer = Completer<int>();
Future.delayed(Duration(seconds: 1))
.then((_) {
completer.complete(42);
});
return completer.future;
}
is equivalent of
Future<int> getValueAsync() async {
await Future.delayed(Duration(seconds: 1));
return 42;
}
In your case:
Future<void> initStream() {
var stream = getStream();
var firstValueReceived = Completer<void>();
stream.listen((val) {
if (!firstValueReceived.isCompleted) {
firstValueReceived.complete();
}
// do some stuff here
});
return firstValueReceived.future;
}