Dart - return data from a Stream Subscription block - asynchronous

I subscribe to a stream with the listen method so I can process the data as it comes in. If I found what I am looking for, I want to cancel the stream and return the data from the stream subscription block.
I have a working implementation but I am using a Completer to block the function from returning immediate and it feels messy, so I want to know if there's a better way to achieve this. Here is my code snippet:
Future<String> _extractInfo(String url) async {
var data = <int>[];
var client = http.Client();
var request = http.Request('GET', Uri.parse(url));
var response = await client.send(request);
var process = Completer();
var interestingData = '';
StreamSubscription watch;
watch = response.stream.listen((value) async {
data.clear(); //clear the previous stale data from the list so searching can be faster
data.addAll(value);
if (
hasInterestingData(data) //search the bytelist for the data I'm looking for
){
interestingData = extractInterestingData(data) //extract the data if it's been found. This is what I want my function to return;
await watch.cancel(); //cancel the stream subscription if the data has been found. ie: Cancel the download
process.complete('done'); //complete the future, so the function can return interestingData.
}
});
watch.onDone(() {
//complete the future if the data was not found...so the function will return an empty string
if (!process.isCompleted) {
process.complete('done');
}
});
await process.future; //blocks the sync ops below so the function doesn't return immediately
client.close();
return interestingData;
}

You should use await for for things like this when you are in an async function.
Future<String> _extractInfo(String url) async {
var client = http.Client();
var request = http.Request('GET', Uri.parse(url));
var response = await client.send(request);
await for (var data in response.stream) {
if (hasInterestingData(data)) {
return extractInterestingData(data); // Exiting loop cancels the subscription.
}
}
return "done";
}
However, while this approach is equivalent to your code, it's probably equally flawed.
Unless you are looking for a single byte, you risk the thing you are looking for being split between consecutive data events. You like do need to retain some part of the previous data array, or some state summarizing what you have already seen, but how much depends on what you are actually looking for.

Related

Call async function in a sync way in Dart

I have this function, want to call it synchronously, because if I call it async then I should use FutureBuilder, which I don't prefer because it has extra flicker if user scrolls too fast:
Future<String> getRealHTML(int chapter) async {
var key = _Html.keys.toList()[chapter];
var val = _Html.values.toList()[chapter];
if (val.Content.startsWith("filename:")) {
EpubContentFileRef value = contentRefHtml[key];
return await value.readContentAsText();
}
return null;
}
readContentAsText() returns a Future, you cannot call it synchronously since it is an I/O request which means it will take time to finish the request, that is why it is called asychronously.

How can I wait for a method that doesn't return a Future?

I am building a screen in an app using an existing model class which I can't modify. The model has a method called refreshList() which fetches some data from an API, processes it, and finally it updates a List member in a model with the fetched data.
I want to know the state of the list - i.e. is it uninitialized, being fetched or already fetched - so that I can update the UI accordingly.
The problem is that refreshList() is not async, but uses an async method internally. Additionally, the internal method doesn't return a Future.
void refreshList(){
if( someCondition ){
_doRefreshList();
}
}
void _doRefreshList() async{
// ... do stuff
http.Response response = await http.get(url);
// ... do some more stuff
}
I would like to do something like
setState((){ _isLoading = true; });
await model.refreshList();
setState((){ _isLoading = false; });
but unfortunately I can't modify the methods in the model to return a Future.
Is there any way to achieve this without modifying refreshList() to be async and return a Future, and modifying _doRefreshList() to return a Future?
No, there is no way to wait for an asynchronous operation to complete unless it provides some way to get a callback when it's done.
The usual way to do that is to return a future. Having a callback function parameter is another approach, but it's rarely better than just returning a future.
In your example, refreshVerList has no way to report when it is done, and neither does _doRefreshList. You will have to add that somehow, and returning a future is the simplest and most consistent way to solve the problem for both functions.
(Technically, the _doRefreshList function does return a future, it's just typed as void. You can cast that future to Future, but I strongly discourage that kinds of shenanigans. If the function author decides to return something else, say null, at a later point, they are perfectly within their right to do since the return type is void.)
You can use Completer.
Completer<Null> onFetchCompleter;
Future<Null> _doRefreshList() async{
onFetchCompleter = Completer();
// ... do stuff
http.Response response = await http.get(url);
// ... do some more stuff
onFetchCompleter.complete();
return onFetchCompleter.future;
}
In the same way you can user a completer in your refreshVerList method.
Create a Future with the function reference as argument:
Future(model.refreshList);
Now you can use await since the above gives you a Future<void>:
setState(() { _isLoading = true; });
await Future(model.refreshList);
setState(() { _isLoading = false; });

In Flutter how can I return an object after completion of Future in nested async functions?

I have a function which calls various other functions that return a Future and after the completion of one Future another method that return a future is called and in the end i need to return a value as a variable but the problem is that function returns NULL instead of the value.
_getLocationPermission() waits and gets the required permission, i have to wait until I get the permission after getting permission I have to call _getCurrentLocation() which will return a Future < LocationData > and i have to pass this object's data to getWeatherDetails() and it will eventually return a Future< String > and i don't know how can i return this string in the return statement.
Future<String> getData() async {
String longitude, latitude, weatherDetails;
_getLocationPermission().then((permissionStatus) {
_getCurrentLocation().then((location) async {
longitude = location.longitude.toString();
latitude = location.latitude.toString();
weatherDetails = await getWeatherDetails(longitude, latitude);
print(longitude);
});
});
return weatherDetails;
}
Thanks!
You seem to be returning a resolve aync response from the getWeatherDetails function not a Future as your function return type shows.
Future<String> getData() async {
var weather;
_getLocationPermission().then((_){
var location = await _getCurrentLocation();
weather = getWeatherDetails(location.longitude.toString(), location.latitude.toString());
})
.catchError((error){
// Handle if location permission fails
});
return weather;
}

Iterate with asynchronous functions

Using the Twitch API and vert.x - I'm looking to continuously send requests to Twitch's API using a WebClient and Twitch's cursor response to go page by page. However I'm not sure how to go back and keep doing queries until a condition is met due to vert.x's asynchronous nature.
Here's my code so far
public void getEntireStreamList(Handler<AsyncResult<JsonObject>> handler) {
JsonObject data = new JsonObject();
getLiveChannels(100, result -> {
if(result.succeeded()) {
JsonObject json = result.result();
String cursor = json.getJsonObject("pagination").getString("cursor");
data.put("data", json.getJsonArray("data"));
if(json.getJsonArray("data").size() < 100) { // IF NOT LAST PAGE
// GO BACK AND DO AGAIN WITH CURSOR IN REQUEST
}
handler.handle(Future.succeededFuture(data));
} else
handler.handle(Future.failedFuture(result.cause()));
});
}
Ideally I'd be able to call getLiveChannels with the cursor String from the previous request to continue the search.
You will need to use Future composition.
Here's my code for your problem:
public void getEntireStreamList(Handler<AsyncResult<JsonObject>> handler) {
JsonArray data = new JsonArray();
// create initial Future for first function call
Future<JsonObject> initFuture = Future.future();
// complete Future when getLiveChannels completes
// fail on exception
getLiveChannels(100, initFuture.completer());
// Create a callback that returns a Future
// for composition.
final AtomicReference<Function<JsonObject, Future<JsonObject>>> callback = new AtomicReference<>();
// Create Function that calls composition with itself.
// This is similar to recursion.
Function<JsonObject, Future<JsonObject>> cb = new Function<JsonObject, Future<JsonObject>>() {
#Override
public Future<JsonObject> apply(JsonObject json) {
// new Future to return
Future<JsonObject> f = Future.future();
// Do what you wanna do with the data
String cursor = json.getJsonObject("pagination").getString("cursor");
data.addAll(json.getJsonArray("data"));
// IF NOT LAST PAGE
if(json.getJsonArray("data").size() == 100) {
// get more live channels with cursor
getLiveChannels(100, cursor, f.completer());
// return composed Future
return f.compose(this);
}
// Otherwise return completed Future with results.
f.complete(new JsonObject().put("data", data));
return f;
}
};
Future<JsonObject> composite = initFuture.compose(cb);
// Set handler on composite Future (ALL composed futures together)
composite.setHandler(result -> handler.handle(result));
}
The code + comments should speak for themselves if you read the Vert.x docs on sequential Future composition.

Dart Future functions with callbacks on requests

I have an issue with understanding Future functions in dart and async programming. The examples I've found in web do not answer my question, so I would be grateful for community help.
I have a function that asks for players messages list:
Future getMessagesInfo(response) async{
dialogs = []; // I need this list to be filled to proceed
messagesList = response["data"];
await getDialogsFunc(messagesList);
renderMessages();
}
It is a callback for a previous request, which doesn't really matters here.
So, here is getDialogsFunc, which purpose is to runs through the list and ask playerInfo for each ID in the response:
Future getDialogsFunc(messagesList) async{
for(var i=0;i<messagesList.length;i++){
if(messagesList[i]["player2"] != player.ID) await rm.getDialogInfo(messagesList[i]["player2"]);
if(messagesList[i]["player1"] != player.ID) await rm.getDialogInfo(messagesList[i]["player1"]);
}
}
Here is getDialogInfo, which actually sends a request for playerInfo and has a callback function that handles received info:
Future getDialogInfo(int id) async{
messages = querySelector("account-messages-tab");
var request = new Request();
Object data = {"id": ServerAPI.PLAYER_INFO, "data":{"id": id}};
await request.sendRequest(data,false, messages.onGotDialogInfo);
}
The request is a simple class, that handles the requests:
class Request{
Future sendRequest(Object data, bool action,Function callback) async{
HttpRequest request = new HttpRequest();
String url = "http://example.com";
await request
..open('POST', url)
..onLoadEnd.listen((e)=> callback(JSON.decode(request.responseText)))
..send(JSON.encode(data));
}
}
And finally here is a callback for the request:
Future onGotDialogInfo(response) async{
List dialog = new List();
dialog.add(response["data"]["id"]);
dialog.add(response["data"]["login"]);
dialogs.add(dialog);
}
In the first function I wanted to run renderMessages() after I have received information about all messages, so that dialogs List should contain relevant information. In my realisation which I tested with breakpoints the renderMessages() functions runs BEFORE onGotDialogInfo callback.
What should I do to wait for the whole cycle getDialogsFunc functions and only then go to renderMessages()?
whenComplete is like finally, it's called no matter whether the request returned normally or with an error. Normally then is used.
getDialogsFunc uses async but doesn't use await which is a bit uncommon. This might be your intention, but I'm not sure
Future getDialogsFunc(messagesList) async {
for(var i=0;i<messagesList.length;i++){
if(messagesList[i]["player2"] != player.ID)
await rm.getDialogInfo(messagesList[i]["player2"]);
if(messagesList[i]["player1"] != player.ID)
await rm.getDialogInfo(messagesList[i]["player1"]);
}
}
getMessagesInfo could then look like:
void getMessagesInfo(response) await {
dialogs = []; // I need this list to be filled to proceed
messagesList = response["data"];
await getDialogsFunc(messagesList)
renderMessages(messagesList);
}
I don't know where Request comes from, therefore hard to tell how that code should look like. It should at least use await for other awaits to work as expected.
Future getDialogInfo(int id) async {
messages = querySelector("account-messages-tab");
var request = new Request();
Object data = {"id": ServerAPI.PLAYER_INFO, "data":{"id": id}};
final response = await request.sendRequest(data,false);
messages.onGotDialogInfo(response)
}
update
class Request{
Future sendRequest(Object data, bool action) async{
Completer completer = new Completer();
HttpRequest request = new HttpRequest();
String url = "http://example.com";
await request
..open('POST', url)
..onLoadEnd.listen((_) {
if (request.readyState == HttpRequest.DONE) {
if (request.status == 200) {
// data saved OK.
completer.complete(JSON.decode(request.responseText)); // output the response from the server
} else {
completer.completeError(request.status);
}
}
})
..send(JSON.encode(data));
return completer.future;
}
}

Resources