3

I am writing a basic programme to teach myself Dart/Flutter. Part of the programme uses the http.dart package to get some data and the http.get command returns a Future value. In order to unpack this value, I need to use an await command, which then changes the execution order of my code. I cannot work out how to preserve the intended execution order whilst using async/await. I am new to this, so appreciate that I am probably missing something obvious.

Code example 1 below uses async/await through a series of functions. This approach gives more or less the correct output order (other than the end of main()), but would mean (I think) that I would need to have an async build() method, which is not valid in Flutter.

// Cascading async methods with local variables and await statements
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;

void main(List<String> arguments) {
  print('Main: start.');
  _build();
  print('Main: end.');
}

// Draw some stuff, make some decisions
void _build() async {
  print('build: Before getName.');
  String name = await _getName();
  print('build: After getName.');
}

// Get some data, make some decisions
Future<String> _getName() async {
  print('getName: before getData');
  String name = await _getData();
  print('getName: after getData');
  double val = Random().nextDouble();
  if (val < 0.5) {
    print('getName: returning body.data');
    return name;
  } else {
    print('getName: returning Bob');
    return 'Bob';
  }
}

// Get the data via an http request
Future<String> _getData() async {
  print('getData: Before http get.');
  final data = await http.get(Uri.parse('http://www.google.co.uk'));
  print('getData: After http get.');
  return data.body;
}

The output from this is (I have truncated the html data that is returned):

Main: start.
build: Before getName.   
getName: before getData  
getData: Before http get.
Main: end.
getData: After http get.
getName: after getData
getName: returning body.data
build: After getName.  Name is: <html data>

The second code example below uses a global variable to capture data in the _getName() method so that I can avoid using async/await in the build() method. This does not give the correct execution order or the correct output.

// Use global variable to receive awaited data and avoid cascading async methods
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;

String name = "";

void main(List<String> arguments) {
  print('Main: start.');
  _build();
  print('Main: end.');
}

// Draw some stuff, make some decisions
void _build() {
  print('build: Before getName.');
  _getName();
  print('build: After getName.  Name is: $name');
}

// Get some data, make some decisions
Future<void> _getName() async {
  print('getName: before getData');
  String data = await _getData();
  print('getName: after getData');
  double val = Random().nextDouble();
  if (val < 0.5) {
    print('getName: setting name = body.data');
    name = data;
  } else {
    print('getName: setting name = Bob');
    name = 'Bob';
  }
  return;
}

// Get the data via an http request
Future<String> _getData() async {
  print('getData: Before http get.');
  final data = await http.get(Uri.parse('http://www.google.co.uk'));
  print('getData: After http get.');
  return data.body;
}

The output from this code is shown below. Note that the build() method completed before _getData and _getName and the name printed in build() is empty in the 5th row.

Main: start.
build: Before getName.   
getName: before getData  
getData: Before http get.
build: After getName.  Name is: 
Main: end.
getData: After http get.
getName: after getData
getName: setting name = body.data

In the third example below, I have tried using .then to ensure that the code in each function only executes after the await command. I didn't think this would work (and it didn't) because I think I have a problem controlling the flow between functions, not a problem controlling the flow within functions, but I thought I should give it a go and I was clutching at straws by this point.

// Use global variable to avoid using await in build() method
// Use .then to ensure that method actions follow await command

import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;

String name = ""; // Global variable for async data return

void main(List<String> arguments) {
  print('Main: start.');
  _build();
  print('Main: end.');
}

// Draw some stuff, make some decisions
void _build() {
  print('build: Before getName.');
  _getName();
  print('build: After getName.  Name is: $name');
}

// Get some data, make some decisions
Future<void> _getName() async {
  print('getName: before getData');
  await _getData().then((data) {
    print('getName: after getData');
    double val = Random().nextDouble();
    if (val < 0.5) {
      print('getName: setting name = body.data');
      name = data;
    } else {
      print('getName: setting name = Bob');
      name = 'Bob';
    }
  });
  return;
}

// Get the data via an http request
Future<String> _getData() async {
  print('getData: Before http get.');
  String value = "";
  await http.get(Uri.parse('http://www.google.co.uk')).then((data) {
    print('getData: After http get.');
    value = data.body;
  });
  return value;
}

The output from this code is shown below. As with the second example, the execution is not in the correct order and the name printed in the build() method is empty.

Main: start.
build: Before getName.   
getName: before getData  
getData: Before http get.
build: After getName.  Name is: 
Main: end.
getData: After http get.
getName: after getData     
getName: setting name = Bob

Ideally, the output from the programme should be:

Main: start.
build: Before getName.   
getName: before getData  
getData: Before http get.
getData: After http get.
getName: after getData     
getName: setting name = Bob
build: After getName.  Name is: Bob 
Main: end.

How do I write my code so that I can use the http.get method and ensure that my code executes in the order that I want? I'll just add that I have read A LOT of stackoverflow questions, flutter documentation and general help online, but not found anything that answers my question so far. Or nothing that I understand. :D Apologies if this is a stupid question. I am a noob at this.

I should have added that this example is an simplification of the problem in a Flutter app I am writing (noughts and crosses). This is checking for a win/draw after each move, then reading data from a DB, updating the results and writing them back to the DB. It also updates the game state to show that the game is over. The problem caused by async/await is that gamestate isn't being updated whilst the functions await the data and the game continues in the "playing" state even though the game is over. Pseudo code of the programme below (this is a bit scrappy, but hopefully it illustrates the problem).

build() {
  checkState(win, draw or continue?);
    if (continue){
      _humanMove();
      _computerMove();
      drawTheWidgets;
    } else {
      drawDifferentWidgets; // New game
    }
}

void _humanMove() async {
    processMove;
    if (win/draw) { 
      await _updateResults; 
      updateGameState(game over);
    }
}

void _computerMove() async {
    processMove;
    if (win/draw) { 
      await _updateResults; 
      updateGameState(game over);
    }
}

results _updateResults() async {
  await http.get data results in database;
  updateWithResult;
  await http.put data results in database;
}
Twelve1110
  • 175
  • 12
  • 1
    Does this answer your question? [What is a Future and how do I use it?](https://stackoverflow.com/questions/63017280/what-is-a-future-and-how-do-i-use-it) – Christopher Moore Dec 14 '21 at 22:10
  • You specifically declared `_build` to have a return type of `void` instead of `Future`. This prevents callers from being notified when it completes (`_build` thus is a "fire-and-forget" function). If you want `main` to wait for `_build` to complete, it must return a `Future`, and `main` must `await` it. – jamesdlin Dec 14 '21 at 23:00
  • I know it's ridiculously old, but this has to be one of the best videos ever to understand Stephan's point below: you need to render something before you have the results (e.g. a placeholder) and something different after you have them. https://www.youtube.com/watch?v=iflV0D0d1zQ&list=PLOU2XLYxmsIIJr3vjxggY7yGcGO7i9BK5&index=3 – Richard Heap Dec 15 '21 at 01:27
  • Thanks @ChristopherMoore - I have read about Futures, but that's a really simple way of explaining it and was useful. It has made me wonder whether I should be using a global variable and testing this to see if it has a value (i.e. the promise is fulfilled) and then letting the code continue once the data is available (or the read failed). Although this feels a bit hacky - I was always taught that global variables were bad. Is this essentially what happens when programmes display a progress bar or spinner? I think that is what I need to be doing (but without the spinner). – Twelve1110 Dec 15 '21 at 08:06
  • Thanks @jamesdlin. I have added some info to my original post to clarify my problem and also responded to Stephan's post below. I did originally go down this route, but in my Flutter app this means that I have to make `build() async`, which means it has to return a `Future`, which means it is no longer a valid override of the `build()` method. – Twelve1110 Dec 15 '21 at 08:08
  • Thanks @RichardHeap - am watching the video now - looks very helpful. – Twelve1110 Dec 15 '21 at 08:09
  • No! There's never any need to make `build` async - indeed you must not! `build` must immediately return a widget (which could contain other widgets) based on the _current_ state of the data you have in hand. `build` can and will be called multiple times by the framework when it feels like it. You can't go off and try to fetch data during build - you have to use the data you already have. – Richard Heap Dec 15 '21 at 12:03
  • Timeline... create stateful widget, maybe kick off fetching its data, build called first time, return a placeholder, data arrives / future completes, parse/validate/whatever the data (and may start fetching more) and call `setState` to tell the framework something's changed, framework calls build again, and this time you return different widgets because you now have data to render. – Richard Heap Dec 15 '21 at 12:05
  • @Twelve1110 Look at the bottom of the accepted answer on the marked duplicate where it refers to `FutureBuilder`s. That's what you should be using when you need the data from a future in `build`. The method proposed by Richard does the same thing, but `FutureBuilder` removes some of the boilerplate. – Christopher Moore Dec 15 '21 at 16:47
  • Ok, thanks for all the comments. I am starting to think I have framed my question badly. I have read about `FutureBuilder` and it is displaying text(?) I am not trying to display text - I am just trying to read a SQL DB record (containing `int` counts of wins, losses and draws), increment one of the values and write it back. I have read various articles about the Flutter lifecycle, but can't see anywhere to put this code other than in the `build` method. Flutter seems to be event driven and build gets called when events happen. Where should I be putting this code if not in `build`? – Twelve1110 Dec 15 '21 at 20:47
  • I should also have mentioned in my original post that I am running my noughts & crosses app in a browser, which I think only calls `build()` in response to user input, whereas (I assume) a mobile/desktop version does regular `build()` calls regardless of user activity. So the timeline mentioned above by @RichardHeap won't be true for a web app? Based on this, I am now thinking I need to architect my app differently, i.e. don't "get, update, put" the data, but just tell the API what the result was and let the backend deal with it. That way the data update can just be fire and forget. – Twelve1110 Dec 16 '21 at 11:37
  • Flutter web absolutely WILL call `build` other than on user input. It will on timers, it will on http responses from the server etc etc. – Richard Heap Dec 16 '21 at 12:31

2 Answers2

2

You need to wait ("await") for all the function calls, see:

void main(List<String> arguments) async {
  print('Main: start.');
  await _build();
  print('Main: end.');
}

// Draw some stuff, make some decisions
Future<void> _build() async {
  print('build: Before getName.');
  await _getName();
  print('build: After getName.  Name is: $name');
}

// Get some data, make some decisions
Future<void> _getName() async {
  print('getName: before getData');
  String data = await Future.delayed(const Duration(milliseconds: 500), () {
    return 'x';
  });
  print('getName: after getData');
  double val = 5;
  if (val < 0.5) {
    print('getName: setting name = body.data');
    name = data;
  } else {
    print('getName: setting name = Bob');
    name = 'Bob';
  }
  return;
}

One comment: I believe it's easier for you to learn to do this right, if you try to learn this not based on console output, but with a Flutter app. You would see that your program goes through different states that actually need time - before, during and after your http request. In your build method of a widget, you would need to provide something to show for for each state of your program. So you actually do not wait (await) for results, but update the state based on the results. And depending on that, your build method is prepared to take the different states and show something adequate. Then async/await is quite nice.

--- adding the following in response to the clarifying comment ---

The build method of a widget is not where you put business logic of your app. Build is called whenever a widget is rebuild (if you have a http request writing in a database, it would be triggered a whole number of times!). Trying to apply the steps you wanted your app to do in your first description with the intended output on the console, I tried to write this clarifying app (and to have different states that your app passes through I included that you have to click the icon button / FloatingActionButton to trigger the process). Please have a look how the build method of the second widget deals with all the different states (it prints them to the console and shows a text - you could do more fancy stuff there of course, based on what the state of your app implies). The real "action" (changes of the state of your app) is happening elsewhere:

import 'package:flutter/material.dart';

class ExampleWidget extends StatefulWidget {
  const ExampleWidget({Key? key}) : super(key: key);

  @override
  _ExampleWidgetState createState() => _ExampleWidgetState();
}

class _ExampleWidgetState extends State<ExampleWidget> {
  String _showWhatsHappening = 'Before getName.';

  Future<String> _getData() async {
    setState(() {
      _showWhatsHappening = 'getData: Before http get.';
    });
    final data = await Future.delayed(const Duration(milliseconds: 500), () {
      return 'Bob';
    });
    setState(() {
      _showWhatsHappening = 'getData: After http get.';
    });
    await Future.delayed(const Duration(milliseconds: 300));
    return data;
  }

  @override
  void initState() {
    print('not main, but initstate: start.');
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Demo',
      theme: ThemeData(brightness: Brightness.light, fontFamily: 'Example'),
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          child: const Icon(Icons.star_outline),
          onPressed: () async {
            setState(() {
              _showWhatsHappening = 'before getData  ';
            });
            final x = await _getData();
            await Future.delayed(const Duration(milliseconds: 300));
            setState(() {
              _showWhatsHappening = 'after getData  ';
            });
            await Future.delayed(const Duration(milliseconds: 300));

            setState(() {
              _showWhatsHappening = 'setting name = $x';
            });
          },
        ),
        body: Center(child: ShowItWidget(showIt: _showWhatsHappening)),
      ),
    );
  }
}

class ShowItWidget extends StatelessWidget {
  final String showIt;
  const ShowItWidget({Key? key, required this.showIt}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print(showIt);
    return Text(showIt);
  }
}

void main() {
  runApp(const ExampleWidget());
}
Stephan
  • 399
  • 1
  • 9
  • Thanks for the quick response. I should have been clearer in my original post. The code examples are based on a Flutter app (noughts and crosses) that I have written to learn Flutter. The problem with using await all the way to the top is that I then have to make build() an async function and this throws the error `('Future Function(BuildContext)') isn't a valid override of 'State.build' ('Widget Function(BuildContext)')`, because `build()` is being overriden and you can't change the return value to a Future. I did refer to this in para. 2, but should have been clearer. – Twelve1110 Dec 15 '21 at 07:43
  • I updated my answer. Please have a look! – Stephan Dec 16 '21 at 05:13
  • Thanks @Stephan. I will study your code and see if I can work out what you are doing. I should also mention that I got my code working using a Stream to make part of the `build()` code wait for the `async` functions to complete, but I understand that this would be bad in a real app. I am also looking at `Isolates`, which seem to offer what I want, but I haven't been able to get them to work yet. My learning curve is pretty steep at the moment! Thanks again for your help. – Twelve1110 Dec 16 '21 at 07:30
  • Ok, that all makes sense (and it works!) I can see that you have made the code following `onPressed:() async` rather than the whole method. I did not know you could do that. :) So... final question, where do I put my business logic? In my noughts and crosses app, I am reading the data once the code determines if someone has won, which comes after the human player clicks a cell (i.e. an interaction starting in `build()` using `onPressed:`) and so the DB code (and test for win code), whilst in a separate function, chains off the `build()` call. Is there a better way to do this? – Twelve1110 Dec 16 '21 at 08:01
  • This is almost a philosophical question :-D. You can find an overview of state management approaches here: https://docs.flutter.dev/development/data-and-backend/state-mgmt/options For a start, I personally would propose to you to start with a ChangeNotifierProvider, see https://flutterbyexample.com/lesson/change-notifier-provider – Stephan Dec 16 '21 at 16:39
  • You've mentioned that you are working with streams. That also sounds good. You could test out a StreamProvider of the provider package. Isolates help a lot for example if you have to do heavy lifting (a lot of computing) in some pure async functions. My approach would be: If you have your app running, but then you want to optimize it, look at Isolates (simplest form is the compute method). However, in relation to your state management Isolates operate similar to async functions. It sounds to me like that path might distract you at the moment. – Stephan Dec 16 '21 at 16:51
  • As far as I understand what you are doing, I think you should dive into the provider package (that's my assessment, not an objective statement; I would base it on my believe that it's easier than other state management approaches and still professional and strong). – Stephan Dec 16 '21 at 16:51
  • My response "This is almost a philosophical question" was in response to your question: "Where do I put my business logic" - just to clarify – Stephan Dec 16 '21 at 16:57
  • Thanks Stephan, those are all really helpful comments. I have learned a lot if the last 48 hours and feel like I going to learn a lot more following up on your suggestions. I have fixed my programme in the short-term by moving the data update logic to the API so that the http command is "fire and forget", but will explore your suggestions to learn more and refine my approach. I will accept your answer because that, along with all your helpful follow-on comments, has helped me to find a way forward. Thank you for all your help. I really appreciate it. :) – Twelve1110 Dec 16 '21 at 18:40
0

For completeness, here is my Dart-specific solution using Streams.

// Use global variable to receive awaited data
// Use StreamController to wait for async methods to finish
import 'dart:async';
import 'dart:math';
import 'package:http/http.dart' as http;

String name = "";
StreamController streamController = StreamController.broadcast();

void main(List<String> arguments) {
  print('Main: start.');
  _build();
  print('Main: end.');
}

// Draw some stuff, make some decisions
void _build() {
  print('build: Before getName.');
  _getName();

  streamController.stream.listen((args) {
    print('build: After getName.  Name is: $name');
  });
}

Future<void> _getName() async {
  print('getName: before getData');
  String data = await _getData();
  print('getName: after getData');

  double val = Random().nextDouble();
  if (val < 0.5) {
    print('getName: setting name = body.data');
    name = data.length.toString();
  } else {
    print('getName: setting name = Bob');
    name = 'Bob ${data.length.toString()}';
  }
  print('gateName.  Name is $name');
  streamController.add(name);
  return;
}

// Get the data via an http request
Future<String> _getData() async {
  print('getData: Before http get.');
  final data = await http.get(Uri.parse('http://www.google.co.uk'));
  print('getData: After http get.');
  return data.body;
}
Twelve1110
  • 175
  • 12