Table of Contents
Introduction
Frequently, applications require the ability to interact with external APIs and databases. With that comes the problem of dealing with code that runs in an order different from how it was written while waiting for certain requests and operations to complete.
Prerequisites
To complete this tutorial, you will need:
- To download and install Flutter.
- To download and install Android Studio _or_ Visual Studio Code.
- It is recommended to install plugins for your code editor:
Flutterextension installed for Visual Studio Code.
This tutorial was verified with Flutter v2.0.6, Android SDK v31.0.2, and Android Studio v4.1.
Understanding Asynchronous Code
With synchronous code, when we send a request for information to an external API, it will take some time before we get a response. Our machine will be waiting for it to complete, halting things that may have nothing to do with the initial request. The problem is we do not want our script to stop running every time something takes a while, but we also wouldn't want it to run anything that relies on the return data prematurely, which could cause an error despite the request being successful.
The best of both worlds would be to set up our logic in a way that allows our machine to work ahead while waiting for a request to return while only letting code dependent on that request run when it is available.
Our app's data is most likely going to be in one of four forms, depending on whether or not they are already available and whether or not they are singular.
This example will explore Futures and Streams.
Setting Up the Project
Once you have your environment set up for Flutter, you can run the following to create a new application:
flutter create <^>flutter_futures_example<^>
Navigate to the new project directory:
cd <^>flutter_futures_example<^>
Using flutter create will produce a demo application that will display the number of times a button is clicked.
Part of this example relies upon the REST Countries API. This API will return information about a country if you provide a country name. For example, here is a request for Canada:
https://restcountries.eu/rest/v2/name/Canada
This will also require the http package.
Open pubspec.yaml in your code editor and add the following plugins:
[label pubspec.yaml]
dependencies:
flutter:
sdk: flutter
<^>http: 0.13.3<^>
Now, open main.dart in your code editor and modify the following lines of code to display a Get Country button:
[label lib/main.dart]
import 'package:flutter/material.dart';
<^>import 'package:http/http.dart' as http;<^>
<^>import 'dart:convert';<^>
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: <^>MyHomePage()<^>,
);
}
}
class MyHomePage extends <^>StatelessWidget<^> {
<^>void getCountry() {}<^>
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
<^>child: MaterialButton(<^>
<^>onPressed: () => getCountry(),<^>
<^>child: Container(<^>
<^>color: Colors.blue,<^>
<^>padding: EdgeInsets.all(15),<^>
<^>child: Text('Get Country', style: TextStyle(color: Colors.white))<^>
<^>),<^>
<^>),<^>
),
);
}
}
At this point, you have a new Flutter project with the http package.
Using then and catchError
Very similar to try…catch in JavaScript, Dart lets us chain methods together so we can easily pass the return data from one to the next and it even returns a Promise-like data type, called Futures. Futures are any singular type of data, like a string, which will be available later.
To use this technique, perform your operations, then just chain .then with our returned data passed in as a parameter, and use it however we want. At that point, you can keep chaining additional .then. For error handling, use a .catchError at the end and throw whatever was passed to it.
Revisit main.dart with your code editor and use .then and .catchError. First, replace void GetCountry() {} with Future GetCountry(country). Then, add a country name to onPressed: () => GetCountry():
[label lib/main.dart]
// ...
class MyHomePage extends StatelessWidget {
<^>Future<^> getCountry(<^>country<^>) {
Uri countryUrl = Uri.http('restcountries.eu', '/rest/v2/name/$country');
<^>http<^>
<^>.get(countryUrl)<^>
<^>.then((response) => jsonDecode(response.body)[0]['name'])<^>
<^>.then((decoded) => print(decoded))<^>
<^>.catchError((error) => throw(error));<^>
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: MaterialButton(
onPressed: () => getCountry(<^>'Canada'<^>),
child: Container(
color: Colors.blue,
padding: EdgeInsets.all(15),
child: Text('Get Country', style: TextStyle(color: Colors.white))
),
),
),
);
}
}
Save your changes and run the application in a simulator. Then, click the Get Country button. Your console will log the name of the country.
Using async and await
An alternative syntax that many find to be much for readable is Async/Await.
Async/Await works exactly the same as in JavaScript, we use the async keyword after our function name and add the await keyword before anything that needs some time to run, like our get request.
Revisit main.dart with your code editor and use async and await. Now everything after it will be run when a value has been returned. For error handling, we can throw the error in a try/catch block.
[label lib/main.dart]
// ...
class MyHomePage extends StatelessWidget {
Future getCountry(country) <^>async<^> {
Uri countryUrl = Uri.http('restcountries.eu', '/rest/v2/name/$country');
<^>try {<^>
<^>http.Response response = await http.get(countryUrl);<^>
<^>Object decoded = jsonDecode(response.body)[0]['name'];<^>
<^>print(decoded);<^>
<^>} catch (e) { throw(e); }<^>
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: MaterialButton(
onPressed: () => getCountry('Canada'),
child: Container(
color: Colors.blue,
padding: EdgeInsets.all(15),
child: Text('Get Country', style: TextStyle(color: Colors.white))
),
),
),
);
}
}
Save your changes and run the application in a simulator. Then, click the Get Country button. Your console will log the name of the country.
Using Streams
Something special with Dart is its use of Streams for when we have many values being loaded asynchronously. Instead of opening a connection once, like with our GET request, we can make it stay open and ready for new data.
Since our example would get a bit too complicated by setting it up with a backend that allows Streams, like using Firebase or GraphQL, we'll simulate a change in a chat application database by emitting a new 'message' every second.
We can create a Stream with the StreamController class, which works similarly to a List, since it behaves like a List of Futures.
We can control our Stream with the properties on stream, like listen and close to start and stop it.
[warning]
Warning: It's important to always use close() when your widget is removed. Streams will run continuously until they are shut off and will eat away at computing power even when the original widget is gone.
Now, open main.dart in your code editor and replace the following lines of code to import dart:async and use a StatefulWidget:
[label lib/main.dart]
import 'package:flutter/material.dart';
<^>import 'dart:async';<^>
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends <^>StatefulWidget<^> {
@override
<^>_MyHomePageState createState() => _MyHomePageState();<^>
}
<^>class _MyHomePageState extends State<MyHomePage> {<^>
<^>StreamController<String> streamController = StreamController();<^>
<^>void newMessage(int number, String message) {<^>
<^>final duration = Duration(seconds: number);<^>
<^>Timer.periodic(duration, (Timer t) => streamController.add(message));<^>
<^>}<^>
<^>void initState() {<^>
<^>super.initState();<^>
<^>streamController.stream.listen((messages) => print(messages));<^>
<^>newMessage(1, 'You got a message!');<^>
<^>}<^>
<^>void dispose() {<^>
<^>streamController.close();<^>
<^>super.dispose();<^>
<^>}<^>
<^>@override<^>
<^>Widget build(BuildContext context) {<^>
<^>return Scaffold(<^>
<^>body: Center(<^>
<^>child: Container(<^>
<^>padding: EdgeInsets.all(15),<^>
<^>child: Text('Streams Example'),<^>
<^>),<^>
<^>),<^>
<^>);<^>
<^>}<^>
<^>}<^>
This code will continuously print out You got a message in the console.
Standard Streams can be a bit limited in that they only allow for one listener at a time. Instead, we can use the broadcast property on the StreamController class to open up multiple channels.
[label lib/main.dart]
// ...
StreamController<String> streamController = StreamController<^>.broadcast()<^>;
// ...
void initState() {
super.initState();
<^>streamController.stream.listen((messages) => print('$messages - First'));<^>
<^>streamController.stream.listen((messages) => print('$messages - Second'));<^>
newMessage(1, 'You got a message!');
}
// ...
This code will continuously print out You got a message - First and You got a message - Second in the console.
Conclusion
In this article, you explored how Dart, particularly for Flutter, works with asynchronous requests. Asynchronous programming in Dart will allow you to start developing intelligent and dynamic apps.
If you'd like to learn more about Flutter, check out our Flutter topic page for exercises and programming projects.