I need to repeatedly call an asynchronous function in Dart, let's call it expensiveFunction, for a variable number of arguments. However, since each call is quite memory consuming, I cannot afford to run them in parallel. How do I force them to run in sequence?
I have tried this:
argList.forEach( await (int arg) async {
Completer c = new Completer();
expensiveFunction(arg).then( (result) {
// do something with the result
c.complete();
});
return c.future;
});
but it hasn't had the intended effect. The expensiveFunction is still being called in parallel for each arg in argList. What I actually need is to wait in the forEach loop until the expensiveFunction completes and only then to proceed with the next element in the argList. How can I achieve that?
You'll want to use a classic for loop here:
doThings() async {
for (var arg in argList) {
await expensiveFunction(arg).then((result) => ...);
}
}
There are some nice examples on the language tour.
Related
In Dart, given an async method
Future transformObject(T object);
both of the following are legal:
Future transformData(Iterable<T> objects) async {
await Future.wait(objects.map((o) => transform(o));
}
and
Future transformData(Iterable<T> objects) async {
await Future.wait(objects.map((o) async { await transform(o); });
}
Is there any difference betwen the two, what is it and which one should I go for if I just want to transform all data objects asynchronously?
If transsform returns a Future, then there is no meaningful difference between transform, (o) => transform(o) and (o) async => await transform(o);. The more wrappings you add, the more overhead, but it's basically the same.
Your function, (o) async { await transform(o); } doesn't return the result of the future that transform returns, but if you don't need that value, then it shouldn't matter. (The name does suggest the you care about the value, though.) You'll still wait for that future to complete before being done.
What you could (possibly should) go for:
Future<List> transformData(Iterable<T> objects) =>
Future.wait(objects.map(transform));
I've enabled the dart 2.8 experimental null saftey.
I have the following exiting code.
StreamSubscription<String> subscription;
subscription =
response.transform(Utf8Decoder()).transform(LineSplitter()).listen(
(line) async {
result += line;
},
onDone: () async {
unawaited(subscription.cancel());
completer.complete(result);
},
);
With null saftey enabled I get a error in the 'onDone' method where it calls subscription.cancl
"The expression is nullable and must be null-checked before it can be used.
Try checking that the value isn't null before using it.",
I can fix the problem by putting a conditional before the call to cancel, but this seems unnecessary as in reality subscription can never be null.
Is there a coding pattern that allows subscription to be declared as non-null?
The problem here is that the read of subscription happens at a place where it's still potentially unassigned. It isn't, actually, but we only know that because the listen method promises not to call any of the callbacks before returning. The compiler can't see that. So, you need to move the reading to after the assignment.
What I'd do to make this listen call work:
var buffer = StringBuffer(result);
var subscription = response
.transform(Utf8Decoder())
.transform(LineSplitter())
.listen((line) {
buffer.write(line);
});
subscription.onDone(() {
completer.complete(buffer.toString());
});
I removed the async from the callbacks because it is not needed. All it does to make these functions async is to return a future that no-one would ever look at.
In general, the callbacks on Stream and Future should have non-async callbacks.
I also removed the subscription.cancel from the onDone event handler. If you get a "done" event, the subscription is done, there is no need to cancel it.
I also added a string buffer to avoid the quadratic time and space complexity of repeated string concatenation.
Looking at the code, you seem to be concatenating lines right after splitting them, maybe all you need is:
response.transform(Utf8Decoder()).join("").then(completer.complete);
I'll assume for now that the splitting+joining is necessary.
In that case, what I'd actually prefer to do instead is of using listen is:
var buffer = StringBuffer();
response
.transform(Utf8Decoder())
.transform(LineSplitter())
.forEach((line) {
buffer.write(line);
}).then(() {
completer.complete(buffer.toString());
}, onError: (e, s) {
completer.completeError(e, s);
});
or, if in an async function:
try {
var buffer = StringBuffer();
await for (var line in response.transform(Utf8Decoder()).transform(LineSplitter())) {
buffer.write(line);
}
completer.complete(buffer.toString());
} catch(e, s) {
completer.completeError(e, s);
}
I need to yield a list for a function; however, I want to yield the list from within a callback function, which itself is inside the main function - this results in the yield statement not executing for the main function, but rather for the callback function.
My problem is very similar to the problem that was solved here: Dart Component: How to return result of asynchronous callback? but I cannot use a Completer because I need to yield and not return.
The code below should describe the problem better:
Stream<List<EventModel>> fetchEvents() async* { //function [1]
Firestore.instance
.collection('events')
.getDocuments()
.asStream()
.listen((snapshot) async* { //function [2]
List<EventModel> list = List();
snapshot.documents.forEach((document) {
list.add(EventModel.fromJson(document.data));
});
yield list; //This is where my problem lies - I need to yield for function [1] not [2]
});
}
Instead of .listen which handles events inside another function you can use await for to handle events inside the outer function.
Separately - you might want to reconsider the pattern when you yield List instances that are still getting populated inside an inner stream callback...
Stream<List<EventModel>> fetchEvents() async* {
final snapshots =
Firestore.instance.collection('events').getDocuments().asStream();
await for (final snapshot in snapshots) {
// The `await .toList()` ensures the full list is ready
// before yielding on the Stream
final events = await snapshot.documents
.map((document) => EventModel.fromJson(document.data))
.toList();
yield events;
}
}
I would like to add a suggestion for improvement here. The suggested await for solution should be avoided in some cases as it is non dismissible listener and it newer stops listening so this might lead to memory leaks. You could as well use .map to transform the stream yield results like so (havent tried to compile it, but main idea should be clear):
Stream<List<EventModel>> fetchEvents() { // remove the async*
Firestore.instance
.collection('events')
.getDocuments()
.asStream()
.map((snapshot) { // use map instead of listen
List<EventModel> list = List();
snapshot.documents.forEach((document) {
list.add(EventModel.fromJson(document.data));
});
return list; // use return instead of yield
});
}
Is it possible to block a function call that returns a future?
I was under the impression calling .then() does it, but that's not what I'm seeing in my output.
print("1");
HttpRequest.getString(url).then((json) {
print("2");
});
print("3");
What I'm seeing in my output is:
1
3
2
The getString method doesn't have an async that would allow me to await it and then executes asynchronously in any case.
static Future<String> getString(String url,
{bool withCredentials, void onProgress(ProgressEvent e)}) {
return request(url, withCredentials: withCredentials,
onProgress: onProgress).then((HttpRequest xhr) => xhr.responseText);
}
How do I make it blocking without placing an infinite while loop before step 3 waiting for step 2 to be completed (not that it would work anyways due to the single thread nature of Dart)?
The above HttpRequest loads a config.json file that determines how everything works in the app, if the request for a field in the config is done before the config.json file is done loading, it causes errors, so I need to wait until the file is done loading before I allow calling getters on the fields of the class or getters needs to wait for the once-off loading of the config.json file.
Update, this is what I eventually did to make it work after Günter suggested I use a Completer:
#Injectable()
class ConfigService {
Completer _api = new Completer();
Completer _version = new Completer();
ConfigService() {
String jsonURI =
"json/config-" + Uri.base.host.replaceAll("\.", "-") + ".json";
HttpRequest.getString(jsonURI).then((json) {
var config = JSON.decode(json);
this._api.complete(config["api"]);
this._version.complete(config["version"]);
});
}
Future<String> get api {
return this._api.future;
}
Future<String> get version {
return this._version.future;
}
}
And where I use the ConfigService:
#override
ngAfterContentInit() async {
var api = await config.api;
var version = await config.version;
print(api);
print(version);
}
Now I get blocking-like functionality without it actually blocking.
There is no way to block execution until asynchronous code completes. What you can do is to chain successive code so that it is not executed before the async code is completed.
One way to chain is then
print("1");
HttpRequest.getString(url) // async call that returns a `Future`
.then((json) { // uses the `Future` to chain `(json) { print("2"); }`
print("2");
});
print("3"); // not chained and therefore executed before the `Future` of `getString()` completes.
An async call is just scheduling code for later execution. It will be added to the event queue and when the tasks before it are processed it itself will be executed. After an async call is scheduled the sync code `print("3") is continued.
In your case HttpRequest.getString() schedules a call to your server and registers (json) { print("2") as callback to be called when the response from the server arrives. Further execution of the application doesn't stall until the response arrives and there is no way to make that happen. What instead happens is that sync code is continued to be executed (print("3")).
If your currently executed sync code reaches its end, then the next scheduled task is processed the same way.
then() schedules the code (json) { print("2"); } to be executed after getString() completed.
await
async and await just make async code look more like sync code but otherwise it is quite the same and will be translated under the hood to xxx.then((y) { ... }).
I'm not sure I understand what you're trying to achieve, but it looks to me you want to do this:
myFunction() async {
print("1");
final json = await HttpRequest.getString(url);
print("2");
print("3");
}
async statement is only needed in the consumer function. In other words, producer functions doesn't need to have async, they only need to return a Future.
you should be able to do this:
Future consumerFunc() async {
print("1");
var response = await HttpRequest.getString(url);
print("2");
print("3");
}
and it should result:
1
2
3
Note: await replaces then methods
Suppose our goal is to build a List which we will call storedResult.
We have a Future<List> returning function called multiple times:
Future<List> getList()...
Suppose that we will call our getList() function for the unknown multiple times and want to store the result of each List into our storedResult:
List storedResult = [];
someOtherList.forEach((element)){
getList().then((resultingList)){
storedResult.addAll(resultingList);
}
}
print(storedResult);
If we run this code, it will print as []. How do we express this so that the result is rather:
[resultingList, resultingList, ..., ]
Remember that Futures return immediately, but their associated callbacks run asynchronously. Specifically, the work that getList() does (and everything that then does) do not happen until execution returns to the event loop.
Your problem is that you're trying to print the result of an asynchronous operation synchronously, and that won't work.
If you want to use the stored result, then what you should do is something like:
Future<List<List>> globFutures(List someOtherList) {
List<Future> futures = [];
someOtherList.forEach((element) {
futures.add(getList());
});
// Automatically completes to a List containing all the
// results of all of the futures.
return Future.wait(futures);
}
main() {
var storedResults = [];
globFutures(someOtherList)
.then((List<List> results) {
storedResults = results;
// Run code in here that depends on storedResults being available.
print(storedResults);
});
// Do **NOT** use storedResults here, as it will be [].
// The Future functions have not yet run!
assert(storedResults == []);
};