You may have heard that Dart is single threaded. Coming from a Java/Android background, it can be confusing. How do you handle async code? What concept replaces Android’s AsyncTask and Java’s threads? And how do you implement a callback?

The concept is called Futures. A Future completes with either a value or an error. You use a try/catch statement to catch the error. To create a Future, you use a Completer.

This all sounds very abstract, so let’s dive into an example.

In this code tutorial, we will set up a screen with 2 separate widgets. Both widgets use the same data repository as their data source. The data repository needs to make an API call to initialise its data. While it does so, the widgets show they are waiting for the data. When the data is ready, the widgets display it.

We will use a public API for the data, for exchange rates of the USD and GBP against the Euro. The first widget will show the USD exchange rate, the second the GBP rate. Both rates are obtained in one single call to https://exchangeratesapi.io/api/latest.

Setting up the app

To follow the code tutorial, create a new app as follows.

Create app flutter create asyncexample 1 flutter create asyncexample

If you’re unsure how to set up a Flutter app, check out Getting started with Flutter official tutorial.

Firstly, we create a Material app in main.dart, which will launch the HomePage widget.

main.dart import 'package:flutter/material.dart'; import 'home_page.dart'; void main() { runApp(new MyApp()); } class MyApp extends StatelessWidget { // This widget is the root of your application. @override Widget build(BuildContext context) { return new MaterialApp( title: 'Async Code Example', theme: new ThemeData( primaryColor: const Color(0xFF43a047), accentColor: const Color(0xFFffcc00), primaryColorBrightness: Brightness.dark, ), home: new HomePage(), ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 import 'package:flutter/material.dart' ; import 'home_page.dart' ; void main ( ) { runApp ( new MyApp ( ) ) ; } class MyApp extends StatelessWidget { // This widget is the root of your application. @ override Widget build ( BuildContext context ) { return new MaterialApp ( title : 'Async Code Example' , theme : new ThemeData ( primaryColor : const Color ( 0xFF43a047 ) , accentColor : const Color ( 0xFFffcc00 ) , primaryColorBrightness : Brightness . dark , ) , home : new HomePage ( ) , ) ; } }

Secondly, we create home_page.dart. It uses 2 widgets: Widget1 and Widget2.

home_page.dart import 'package:flutter/material.dart'; import 'package:asyncexample/widget1.dart'; import 'package:asyncexample/widget2.dart'; class HomePage extends StatefulWidget { HomePage({Key key}) : super(key: key); @override _HomePageState createState() => new _HomePageState(); } class _HomePageState extends State<HomePage> { @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar( title: new Text("Async Code Example"), ), body: new ListView( children: <Widget>[ new Widget1(), new Widget2() ], ), ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 import 'package:flutter/material.dart' ; import 'package:asyncexample/widget1.dart' ; import 'package:asyncexample/widget2.dart' ; class HomePage extends StatefulWidget { HomePage ( { Key key } ) : super ( key : key ) ; @ override _HomePageState createState ( ) = > new _HomePageState ( ) ; } class _HomePageState extends State < HomePage > { @ override Widget build ( BuildContext context ) { return new Scaffold ( appBar : new AppBar ( title : new Text ( "Async Code Example" ) , ) , body : new ListView ( children : < Widget > [ new Widget1 ( ) , new Widget2 ( ) ] , ) , ) ; } }

Lastly, we create the 2 widgets, in widget1.dart and widget2.dart respectively.

widget1.dart import 'package:flutter/material.dart'; class Widget1 extends StatefulWidget { Widget1({Key key}) : super(key: key); @override _Widget1State createState() => new _Widget1State(); } class _Widget1State extends State<Widget1> { @override Widget build(BuildContext context) { return new Card( margin: const EdgeInsets.all(8.0), child: new Container( padding: const EdgeInsets.all(16.0), child: new Text('Widget 1 not implemented yet'), ), ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import 'package:flutter/material.dart' ; class Widget1 extends StatefulWidget { Widget1 ( { Key key } ) : super ( key : key ) ; @ override _Widget1State createState ( ) = > new _Widget1State ( ) ; } class _Widget1State extends State < Widget1 > { @ override Widget build ( BuildContext context ) { return new Card ( margin : const EdgeInsets . all ( 8.0 ) , child : new Container ( padding : const EdgeInsets . all ( 16.0 ) , child : new Text ( 'Widget 1 not implemented yet' ) , ) , ) ; } }

widget2.dart import 'package:flutter/material.dart'; class Widget2 extends StatefulWidget { Widget2({Key key}) : super(key: key); @override _Widget2State createState() => new _Widget2State(); } class _Widget2State extends State<Widget2> { @override Widget build(BuildContext context) { return new Card( margin: const EdgeInsets.all(8.0), child: new Container( padding: const EdgeInsets.all(16.0), child: new Text('Widget 2 not implemented yet'), ), ); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import 'package:flutter/material.dart' ; class Widget2 extends StatefulWidget { Widget2 ( { Key key } ) : super ( key : key ) ; @ override _Widget2State createState ( ) = > new _Widget2State ( ) ; } class _Widget2State extends State < Widget2 > { @ override Widget build ( BuildContext context ) { return new Card ( margin : const EdgeInsets . all ( 8.0 ) , child : new Container ( padding : const EdgeInsets . all ( 16.0 ) , child : new Text ( 'Widget 2 not implemented yet' ) , ) , ) ; } }

Looking for a Flutter job? Check out my job board dedicated to Flutter at flutterjobs.info

Setting up the data repository

The data repository is a singleton, so both widgets use the same instance. Let’s create a new file data_repository.dart.

data_repository.dart final DataRepository dataRepository = new DataRepository._private(); class DataRepository { DataRepository._private(); } 1 2 3 4 5 6 7 8 final DataRepository dataRepository = new DataRepository . _private ( ) ; class DataRepository { DataRepository . _private ( ) ; }

Now, we can add an init() method, that calls the API. It returns a Future and we use the async keyword; this allows us to use await when calling the API. Then, we parse the result, and store the data in _exchangeRates. If there is any kind of error, _exchangeRates is null.

data_repository.dart import 'dart:async'; import 'package:http/http.dart' as http; import 'dart:convert'; const _API_PATH = "https://exchangeratesapi.io/api/latest"; const HTTP_HEADERS = const { 'User-Agent': 'Flutter Code Tutorial (http://cogitas.net)', 'Accept': 'application/json', }; final DataRepository dataRepository = new DataRepository._private(); class DataRepository { DataRepository._private(); ExchangeRates _exchangeRates; Future init() async { try { _exchangeRates = null; final response = await http.get(_API_PATH, headers: HTTP_HEADERS); if (response.statusCode == 200) { final Map responseJson = json.decode(response.body); if (responseJson.containsKey("rates") && responseJson.containsKey("date")) { Map rates = responseJson["rates"]; if (rates.containsKey("USD") && rates.containsKey("GBP")) { _exchangeRates = new ExchangeRates(responseJson["date"], new ExchangeRate("USD", rates["USD"]), new ExchangeRate("GBP", rates["GBP"])); } } } } catch(e) { } } } class ExchangeRates { final String dateStr; final ExchangeRate rateUSD; final ExchangeRate rateGBP; const ExchangeRates(this.dateStr, this.rateUSD, this.rateGBP); } class ExchangeRate { final String currencyDisplay; final double rate; const ExchangeRate(this.currencyDisplay, this.rate); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import 'dart:async' ; import 'package:http/http.dart' as http ; import 'dart:convert' ; const _API_PATH = "https://exchangeratesapi.io/api/latest" ; const HTTP_HEADERS = const { 'User-Agent' : 'Flutter Code Tutorial (http://cogitas.net)' , 'Accept' : 'application/json' , } ; final DataRepository dataRepository = new DataRepository . _private ( ) ; class DataRepository { DataRepository . _private ( ) ; ExchangeRates _exchangeRates ; Future init ( ) async { try { _exchangeRates = null ; final response = await http . get ( _API_PATH , headers : HTTP_HEADERS ) ; if ( response . statusCode == 200 ) { final Map responseJson = json . decode ( response . body ) ; if ( responseJson . containsKey ( "rates" ) && responseJson . containsKey ( "date" ) ) { Map rates = responseJson [ "rates" ] ; if ( rates . containsKey ( "USD" ) && rates . containsKey ( "GBP" ) ) { _exchangeRates = new ExchangeRates ( responseJson [ "date" ] , new ExchangeRate ( "USD" , rates [ "USD" ] ) , new ExchangeRate ( "GBP" , rates [ "GBP" ] ) ) ; } } } } catch ( e ) { } } } class ExchangeRates { final String dateStr ; final ExchangeRate rateUSD ; final ExchangeRate rateGBP ; const ExchangeRates ( this . dateStr , this . rateUSD , this . rateGBP ) ; } class ExchangeRate { final String currencyDisplay ; final double rate ; const ExchangeRate ( this . currencyDisplay , this . rate ) ; }

The code above includes some simple JSON parsing. If this is unclear to you, check out my code tutorial How to parse JSON in Dart / Flutter.

We initialise the data repository from home_page.dart.

home_page.dart [...] import 'package:asyncexample/data_repository.dart'; class _HomePageState extends State<HomePage> { @override void initState() { super.initState(); dataRepository.init(); } [...] } 1 2 3 4 5 6 7 8 9 10 11 12 13 [ . . . ] import 'package:asyncexample/data_repository.dart' ; class _HomePageState extends State < HomePage > { @ override void initState ( ) { super . initState ( ) ; dataRepository . init ( ) ; } [ . . . ] }

Using a Future instead of a callback

BUT… when is the data ready? How can the widgets know? Instead of using a callback to know when the data is ready, we use a Future to retrieve the data. Let’s implement a getExchangeRates() method in data_repository.dart.

If the data is ready, getExchangeRates() returns the data immediately. If not, a completer is added to a list, its future is returned, and when init() completes, all the completers in the list complete with _exchangeRates value, prompting their futures to complete.

data_repository.dart [...] class DataRepository { [...] bool _ready = false; List<Completer<ExchangeRates>> _listeners = new List<Completer<ExchangeRates>>(); Future<ExchangeRates> getExchangeRates() async { if (_ready) { return _exchangeRates; } else { Completer listener = new Completer(); _listeners.add(listener); return listener.future; } } Future init() async { [...] // Notify all listeners _ready = true; for (var listener in _listeners) { listener.complete(_exchangeRates); } _listeners = new List<Completer<ExchangeRates>>(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 [ . . . ] class DataRepository { [ . . . ] bool _ready = false ; List < Completer < ExchangeRates >> _listeners = new List < Completer < ExchangeRates >> ( ) ; Future < ExchangeRates > getExchangeRates ( ) async { if ( _ready ) { return _exchangeRates ; } else { Completer listener = new Completer ( ) ; _listeners . add ( listener ) ; return listener . future ; } } Future init ( ) async { [ . . . ] // Notify all listeners _ready = true ; for ( var listener in _listeners ) { listener . complete ( _exchangeRates ) ; } _listeners = new List < Completer < ExchangeRates >> ( ) ; } }

Now, in the widgets, we can safely call this new method without blocking the execution thread. Let’s amend widget1.dart and widget2.dart as below. Notice the use of the Future.then() construct in initState(): this is because we can’t use Future initState() async as initState() is an overridden method.

widget1.dart import 'package:flutter/material.dart'; import 'package:asyncexample/data_repository.dart'; [...] class _Widget1State extends State<Widget1> { ExchangeRates _exchangeRates = null; bool _loading; @override void initState() { super.initState(); _loading = true; dataRepository.getExchangeRates().then((result) { setState(() { _exchangeRates = result; _loading = false; }); }); } @override Widget build(BuildContext context) { return new Card( margin: const EdgeInsets.all(8.0), child: new Container( padding: const EdgeInsets.all(16.0), child: _buildCard(), ), ); } Widget _buildCard() { if (_loading) { return new Center(child: new CircularProgressIndicator()); } else { if (_exchangeRates == null) { return new Text("Error loading exchange rates"); } else { return new Text("1 Euro = " + _exchangeRates.rateUSD.rate.toString() + " " + _exchangeRates.rateUSD.currencyDisplay); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import 'package:flutter/material.dart' ; import 'package:asyncexample/data_repository.dart' ; [ . . . ] class _Widget1State extends State < Widget1 > { ExchangeRates _exchangeRates = null ; bool _loading ; @ override void initState ( ) { super . initState ( ) ; _loading = true ; dataRepository . getExchangeRates ( ) . then ( ( result ) { setState ( ( ) { _exchangeRates = result ; _loading = false ; } ) ; } ) ; } @ override Widget build ( BuildContext context ) { return new Card ( margin : const EdgeInsets . all ( 8.0 ) , child : new Container ( padding : const EdgeInsets . all ( 16.0 ) , child : _buildCard ( ) , ) , ) ; } Widget _buildCard ( ) { if ( _loading ) { return new Center ( child : new CircularProgressIndicator ( ) ) ; } else { if ( _exchangeRates == null ) { return new Text ( "Error loading exchange rates" ) ; } else { return new Text ( "1 Euro = " + _exchangeRates . rateUSD . rate . toString ( ) + " " + _exchangeRates . rateUSD . currencyDisplay ) ; } } } }

widget2.dart import 'package:flutter/material.dart'; import 'package:asyncexample/data_repository.dart'; [...] class _Widget2State extends State<Widget2> { ExchangeRates _exchangeRates = null; bool _loading; @override void initState() { super.initState(); _loading = true; dataRepository.getExchangeRates().then((result) { setState(() { _exchangeRates = result; _loading = false; }); }); } @override Widget build(BuildContext context) { return new Card( margin: const EdgeInsets.all(8.0), child: new Container( padding: const EdgeInsets.all(16.0), child: _buildCard(), ), ); } Widget _buildCard() { if (_loading) { return new Center(child: new CircularProgressIndicator()); } else { if (_exchangeRates == null) { return new Text("Error loading exchange rates"); } else { return new Text("1 Euro = " + _exchangeRates.rateGBP.rate.toString() + " " + _exchangeRates.rateGBP.currencyDisplay); } } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 import 'package:flutter/material.dart' ; import 'package:asyncexample/data_repository.dart' ; [ . . . ] class _Widget2State extends State < Widget2 > { ExchangeRates _exchangeRates = null ; bool _loading ; @ override void initState ( ) { super . initState ( ) ; _loading = true ; dataRepository . getExchangeRates ( ) . then ( ( result ) { setState ( ( ) { _exchangeRates = result ; _loading = false ; } ) ; } ) ; } @ override Widget build ( BuildContext context ) { return new Card ( margin : const EdgeInsets . all ( 8.0 ) , child : new Container ( padding : const EdgeInsets . all ( 16.0 ) , child : _buildCard ( ) , ) , ) ; } Widget _buildCard ( ) { if ( _loading ) { return new Center ( child : new CircularProgressIndicator ( ) ) ; } else { if ( _exchangeRates == null ) { return new Text ( "Error loading exchange rates" ) ; } else { return new Text ( "1 Euro = " + _exchangeRates . rateGBP . rate . toString ( ) + " " + _exchangeRates . rateGBP . currencyDisplay ) ; } } } }

What next?

Reading Asynchronous Programming: Futures on Dart’s website is well worth your time. In particular, don’t forget to handle errors! If you want the error to cascade to your UI, do not catch it until you need to handle it (eg show something different in the UI). If, however, you want to swallow errors, eg trying to read data from one place but then using a backup method if it fails, then do remember to use try/catch.

Related