Dart: Futures and Streams

Learn how to use Futures and Streams for writing asynchronous code in dart By Michael Katz.

5 (1) · 1 Review

Download materials
Save for later
Share

Dart, like many other programming languages, has built-in support for asynchronous programming — writing code that runs later. Your program can wait for work to finish and perform other tasks while waiting. This technique is often used to fetch information from a device or server and not block your program.

In this tutorial, you’ll learn how to use Futures and Streams for writing asynchronous code in Dart.

Getting Started

This tutorial is a pure Dart experience so use DartPad at https://dartpad.dev/. DartPad lets you edit and run Dart code in the browser. There is no corresponding Flutter app, but all the concepts and code can be used in a normal Flutter app.

For this tutorial, you’ll simulate an API that returns demographic data on world cities. The program will “load” data from this simulated API as if you were building a mapping or educational app. In reality, the data is all hard-coded and sent after a time delay.

In DartPad, create a new Dart pad.

Select a new Dart file in DartPad

You’ll see a basic main function that prints a count.

Async Basics

Synchronous code has limits. You can only run one chunk of code at a time, which might be problematic if that code takes a long time to run. Asynchronous code lets you write code that responds to events that happen later, such as data becoming available from an API.

For the cities program, display a greeting and load some cities.

Replace the DartPad contents with:

List<String> fetchCityList() {
  print("[SIMULATED NETWORK I/O]");
  return ['Bangkok', 'Beijing', 'Cairo', 'Delhi', 'Guangzhou', 'Jakarta', 'Kolkāta', 'Manila', 'Mexico City', 'Moscow', 'Mumbai', 'New York', 'São Paulo', 'Seoul', 'Shanghai', 'Tokyo'];
}

void printCities(List<String> cities) {
  print("Cities:");
  for (final city in cities) {
    print("   " + city);
  }
}

void main() {
  print("Welcome to the Cities program!");
  final cities = fetchCityList();
  printCities(cities);
}

This has two helper functions: fetchCityList returns a list of cities, and printCities prints that list to the console. The main function displays the greeting and uses the helper method to fetch the cities. For a Flutter app, you might display the city list using a ListView widget.

The fetchCityList function is meant to simulate loading data from the network, but it finishes immediately, which isn’t how an API call normally works.

The order of operations in the main function. Each step completes before the next one.

The order of operations in the main function. Each step completes before the next one.

What if that fetch was slow or error-prone?

Replace the existing main function with the following (try not to read into it yet):

Future<List<String>> fetchSlowCityList() async {
  print("Loading...");
  await Future.delayed(Duration(seconds: 2));
  return fetchCityList();
}

void main() async {
  print("Welcome to the Cities program!");
  final cities = await fetchSlowCityList();
  printCities(cities);
}

Run the code and you’ll feel the delay in the console. The 2-second delay is helpful because you don’t know your end-users’ networking conditions.

This time there is a forced wait to get the results.

This time there is a forced wait to get the results.

If you were to run the above code on the main thread of a Flutter app, your app would be unresponsive while it waits for completion.

Using Futures

Now that you’ve seen the Dart type Future in action, let’s learn how to use it in your own code.

A Future is an object that will return a value (or an error) in the future. It’s critical to understand the Future itself is not the value, but rather the promise of a value.

To see this in practice, replace main with the following:

void main() {
  print("Welcome to the Cities program!");
  // 1
  final future = Future.delayed(
    const Duration(seconds: 3),
    fetchCityList,
  );
  // 2
  print("The future object is actually: " + future.toString());
  // 3
  future.then((cities) {
    printCities(cities);
  });
  // 4
  print("This happens before the future completes.");
}

This example highlights some of the properties and conditions of using futures.

  1. A Future can be built a few ways. A delayed future will execute a provided computation after a specified delay. In this case, the delay is a 3-second Duration, and the computation is a call to fetchCityList.
  2. This print statement is a reminder that a Future is its own type and isn’t the value of the computation.
  3. The then function will execute a callback with a computed value when the future is complete. In this case, the value completed value is the cities variable, and it is then forwarded to the print-out function.
  4. This print statement is a reminder that even though this appears after the call to then in program order. It’ll execute before the completion callback.

If you run the program, you’ll see the output shows the order in which the code is executed.

Welcome to the Cities program!
The future object is actually: Instance of '_Future<List<String>>'
This happens before the future completes.
[SIMULATED NETWORK I/O]
Cities:
   Bangkok
   Beijing
   Cairo
   Delhi
   Guangzhou
   Jakarta
   Kolkāta
   Manila
   Mexico City
   Moscow
   Mumbai
   New York
   São Paulo
   Seoul
   Shanghai
   Tokyo

While the “network” i/o is happening, the final print statement is printed. The city list prints later.

asynchronous diagram

Future States

A Future has two states: uncompleted and completed. An uncompleted Future is one that hasn’t produced a value (or error) yet. A completed Future is a Future after computing its value.

In this next example, you’ll use a Timer to show a loading indicator text in the console. At the top of the DartPad, add:

import 'dart:async';

This will import the async package so you can use a timer. Next, replace main with:

void main() {
  // 1
  final future = fetchSlowCityList();

  // 2
  final loadingIndicator = Timer.periodic(
    const Duration(milliseconds: 250),
    (timer) => print("."));

  // 3
  future.whenComplete(() => loadingIndicator.cancel());

  // 4
  future.then((cities) {
    printCities(cities);
  });
}

This code illustrates the completed state by assigning a whenComplete callback. This code does the following:

  1. Reuses the helper function to create a city list Future.
  2. The timer will drop a “.” in the console every 250 milliseconds.
  3. When the loading future is complete, it cancels the timer so it stops printing dots.
  4. When the future completes, print the completed value to the console.

whenComplete is called when the future finishes, regardless of whether it produced a value or an error. It’s a good place to clean up state, such as canceling a timer. On the other hand, then is only called when a value is present.