Can't yield in forEachAsync inside Stream in dart/flutter - flutter

I have a forEachAsync inside an async* Stream and can't yield.
Stream<ProjectState> _mapProjectSelectedEventToState(ProjectSelected event) async* {
try {
yield ProjectLoading(
message: 'Fetching database',
fetchedCount: 0,
totalCount: 1,
);
await forEachAsync(fileModels, (FileEntity fileModel) async {
await downloader.download(filename: fileModel.hashName);
_totalMediaFilesFetched++;
//// ERROR - THIS DOES NOT WORK ////
yield (ProjectLoadingTick(
_totalMediaFiles,
_totalMediaFilesFetched,
));
}, maxTasks: 5);
} catch (error, stacktrace) {
yield ProjectFailure(error: error);
}
}
I've tried other means by dispatching the message and converting it to a state but it doesn't work as well. It seems like the whole app is blocked by this await forEachAsync.
I'm using the bloc pattern which reacts to the emited ProjectStates based on the current ProjectSelected event

Your attempt doesn't work because you're using yield in a callback, not in the function that's returning a Stream. That is, you're attempting the equivalent of:
Stream<ProjectState> _mapProjectSelectedEventToState(ProjectSelected event) async* {
...
await forEachAsync(fileModels, helperFunction);
...
}
Future helperFunction(FileEntity fileModel) async {
...
yield ProjectLoadingTick(...);
}
which doesn't make sense.
Since care about forEachAsync's ability to set a maximum limit to the number of outstanding asynchronous operations,
you might be better off using a StreamController that you can manually add events to:
var controller = StreamController<ProjectState>();
// Note that this is not `await`ed.
forEachAsync(fileModels, (FileEntity fileModel) async {
await downloader.download(filename: fileModel.hashName);
_totalMediaFilesFetched++;
controller.add(ProjectLoadingTick(
_totalMediaFiles,
_totalMediaFilesFetched,
));
},
maxTasks: 5);
yield* controller.stream;

Related

Show receiving progress while multiple API calls

In my app after a user signs in, I am downloading some data from the server and this data can be up to 5000 rows so it takes some time to finish. To better user experience I would like to show the user a LinearProgressIndicator about downloading status.
Things to consider:
This is not one API call, the splash screen triggers an async method and this method calls nearly 10 different API endpoints. Some of them are downloading just 1 row, some of them are downloading 100 rows and others are downloading up to 5000.
In my current implementation, splash screen calls API methods with async, so calls make in sequence. The implementation contains only a CircularProgressIndicator to show the user something is happening on the backend.
I tried to implement Stream because I think I need a Stream to follow which API call started and what is the current situation.
Here is how I try:
//SplashScreen
final viewModel = HomeViewModel();
final stream = viewModel.initializeData();
stream.listen((event) {
setState(() {
downloadingItem = event.itemName;
uploadProgress = event.received / event.total;
});
});
//HomeViewModel
Stream<ReceiveProgress> initializeData() async* {
bigService.initializeData().listen((event) async* {
yield event;
});
await veryBigService.initializeData(onReceiveProgress: (recieved, total) async* {
yield ReceiveProgress("Big Data", recieved, total);
});
await middleSizeService.initializeData(pilotID!);
await anotherBigService.sync();
await authentication.checkSubscription();
}
bigService.initializeData() typically calls three different APIs on sequence, so I think I should apply the same Stream login for that.
Stream<RecieveProgress> initializeData() async* {
bool result = await InternetConnectionChecker().hasConnection;
if (!result) return;
if (typeBox.isEmpty()) {
api.getAllTypes(
onReceiveProgress: (count, total) async* {
yield ReceiveProgress("Types", count, total);
},
).then((value) {
if (value.isFailure) return;
typeBox.putMany(value.data!, mode: PutMode.insert);
syncService.updateSyncTime<ItemType>();
});
}
if (manufacturerBox.isEmpty()) {
api.getAllManufacturers(
onReceiveProgress: (count, total) async* {
yield ReceiveProgress("Manufacturers", count, total);
},
).then((value) {
if (value.isFailure) return;
manufacturerBox.putMany(value.data!, mode: PutMode.insert);
syncService.updateSyncTime<Manufacturer>();
});
}
if (box.isEmpty()) {
api.getAll(
onReceiveProgress: (count, total) async* {
yield ReceiveProgress("Items", count, total);
},
).then((value) {
if (value.isFailure) return;
purgeAndSaveAll(value.data!);
});
}
}
As you can see there is a class named ReceiveProgress. Here is how I defined it.
class ReceiveProgress {
final String itemName;
final int received;
final int total;
RecieveProgress(
this.itemName,
this. received,
this.total,
);
}
At the end, I want to show the results of multiple API call progress in the splash screen but I think I am stuck.

Migrate to BLoC 7.2- Nested Streams - yield* inside other stream

I'm migrating a project from Bloc 7.0 to 7.2
I have an issue trying handle the migration of this following Stream since it is calling another Stream within it self :
Stream<CustomerState> _mapUpdateNewsletter({...}) async* {
try {
[...]
yield* _mapGetCustomer(); // Calling another Stream here
Toast.showSuccess(message: successMessage);
} ...
}
Here is what the called Stream used to look like
Stream<CustomerState> _mapGetCustomer() async* {
try {
final customer = await _customerRepository.getCustomer();
yield state.getCustomerSuccess(customer);
} catch (error, stackTrace) {
ApiError.handleApiError(error, stackTrace);
}
}
Here is what I migrated it to :
Future<void> _onGetCustomer(
GetCustomer event, Emitter<CustomerState> emit) async {
try {
final customer = await _customerRepository.getCustomer();
emit(state.getCustomerSuccess(customer));
} catch (error, stackTrace) {
ApiError.handleApiError(error, stackTrace);
}
}
How am I suppose to call it now in Bloc 7.2 ?
Future<void> _onUpdateNewsletter(UpdateNewsletter event, Emitter<CustomerState> emit) async {
try {
...
yield* _onGetCustomer; // How do I call this async future here?
Toast.showSuccess(message: event.successMessage);
} ...
}
in the new version of the bloc, you don't have to write stream functions. you have a function called emit and calling this function and passing the new state is possible from every function in your bloc. so remove yield* and just call _onGetCustomer function and from there emit your new state.

Can Dart Streams emit a value if the stream is not done within a duration?

I am working on a Flutter app using blocs to control the state of the view. I want to call and external API and if it responds quickly, show the results right away by yielding the result state. However, if the call takes more than, say, 5 seconds, I would like to yield a state indicating that the response is taking a while while still waiting for the API to return. How can I do this with Dart Streams, either natively or with RxDart?
This can be accomplished using Stream.timeout. Thanks #pskink!
Stream<String> delayedCall() async* {
yield 'Waiting';
final apiCall = Future.delayed(Duration(seconds: 5)).then((_) => 'Complete');
yield* Stream.fromFuture(apiCall).timeout(
Duration(seconds: 3),
onTimeout: (eventSink) => eventSink.add('Still waiting'),
);
}
void main() {
final stream = delayedCall();
stream.listen(print);
}
All the Futureshave a delayed timeout that you can use for example
try{
var timer = Timer(Duration(seconds: 3), () {
yield SlowAPI();
});
YourFeature.whenComplete((){
timer.cancel();
});
yield Success();
}catch(e){
yield Error();
}

How can I cancel a 'await for' in a flutter / dart bloc

I am using a stream to read out location data in a bloc. I have a start and a stop event. In the stop method, I cancel the stream subscription. When I use listen to a stream to yield the state the inside where the yield statement is never gets called.
Stream<LocationState> _start() async* {
_locationSubscription = location.onLocationChanged.listen(
(location) async* {
if (location.isNotNull) {
yield LocationState.sendData(location: updateLocation(location));
}
},
);
//send one initial update to change state
yield LocationState.sendData(
location: updateLocation(await Location().getLocation()));
}
Stream<LocationState> _stop() async {
await _locationSubscription?.cancel();
_locationSubscription = null;
yield LocationState.stoped();
}
When I replace the listen to await for I don't see any way
to stop this from yielding events because the subscription handle is gone.
Any ideas? Any explanations?
Stream<LocationState> _start() async* {
await for (LocationData location in location.onLocationChanged) {
if (location.isNotNull) {
yield LocationState.sendData(location: updateLocation(location));
}
}
//send one initial update to change state
yield LocationState.sendData(
location: updateLocation(await Location().getLocation()));
}
The problem is that I did not understand the behavior of yield completely.
Also, the dart framework has some shortcomings.
The problem was discussed in detail with the dart makers, here.
https://github.com/dart-lang/sdk/issues/42717
and here
https://github.com/felangel/bloc/issues/1472

How to work with async* functions in Dart

I am using flutter_bloc library.
In the bloc, the mapEventToState method is an async* function which returns Stream<BlocState>.
From this function I am calling other async* functions like this yield* _handleEvent(event)
In such method, I am calling some Future returns functions but in the Future then() function it wont let me call other yield* functions.
Here is an example:
Stream<BlocState> mapEventToState(BlocEvent event) async*{
yield* _handlesEvent(event); //This calls to worker method
}
Stream<BlocState> _handleEvent(BlocEvent event) async* {
_repository.getData(event.id).then((response) async* { //Calling Future returned function
yield* _processResult(response); //This won't work
}).catchError((e) async* {
yield* _handleError(e); //This won't work either
});
Response response = await _repository.getData(event.id); //This do works but I want to use it like above, is it possible?
yield* _processResult(response); //This do works
}
The question is however, how to combine between Future and Stream in dart.
I could use await _repository.getData which works. but then I won't catch the error.
await is just syntactic sugar for .then(), and putting await in a try-catch block is syntactic sugar for using .catchError. Things that you can do one way can be done with the other.
In your first version that uses .then()/.catchError(), your function doesn't return anything.
Your callbacks won't work because you're using yield* in them, but you haven't specified the callbacks with sync* or async*. To avoid name collisions, the yield keyword requires them (in the same way that await requires a function use async or async*).
Here's a version that should work with .then() and .catchError():
Stream<BlocState> _handleEvent(BlocEvent event) async* {
yield* await _repository.getData(event.id).then((response) async* {
yield* _processResult(response);
}).catchError((e) async* {
yield* _handleError(e);
});
}
Note that the callbacks don't need to use yield*; they could just return their Streams directly:
Stream<BlocState> _handleEvent(BlocEvent event) async* {
yield* await _repository.getData(event.id).then((response) {
return _processResult(response);
}).catchError((e) {
return _handleError(e);
});
}
But (as everyone else has noted) using await instead of the Future API simplifies the whole thing (especially since we're already using await anyway):
Stream<BlocState> _handleEvent(BlocEvent event) async* {
try
response = await _repository.getData(event.id);
yield* _processResult(response);
} catch (e) {
yield* _handleError(e);
}
}
See https://dartpad.dartlang.org/fc1ff92e461754bdb35b998e7fbb3406 for a runnable example.
Try using a try-catch block instead. It works for me with await operations.
To handle errors in an async function, use try-catch:
try {
Response response = await _repository.getData(event.id)
} catch (err) {
print('Caught error: $err');
}