Tutorial

How To Get Started with Futures and Streams in Dart and Flutter

Flutter

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.

In this article, you will explore how Dart, particularly for Flutter, works with asynchronous requests.

Prerequisites

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.

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.

A table with four cells - two rows and two columns. There are two horizontal labels: the first is Available and the second is Waiting. There are two vertical labels: the first is Singular and the second is Plural. Available and Plural needs List/Array data. Available and Singular needs String data. Waiting and Singular needs Future data. Waiting and Plural needs Stream data.

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:

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:

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():

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.

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: 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:

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.

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.

Creative Commons License