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.
In this article, you will explore how Dart, particularly for Flutter, works with asynchronous requests.
To complete this tutorial, you will need:
This tutorial was verified with Flutter v2.0.6, Android SDK v31.0.2, and Android Studio v4.1.
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.
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:
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:
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatelessWidget {
void getCountry() {}
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.
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()
:
// ...
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));
}
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.
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.
// ...
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); }
}
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.
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: 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
:
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
_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();
}
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.
// ...
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.
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.
Thanks for learning with the DigitalOcean Community. Check out our offerings for compute, storage, networking, and managed databases.
This textbox defaults to using Markdown to format your answer.
You can type !ref in this text area to quickly search our full set of tutorials, documentation & marketplace offerings and insert the link!