Search⌘ K
AI Features

Introduction to Streams (Multiple Asynchronous Values)

Explore how to manage multiple asynchronous values in Dart using streams. Understand creating stream generators with async* and yield, and consuming streams with await for loops to handle continuous data effectively for responsive Flutter apps.

Up to this point, we have used futures to handle asynchronous tasks. A Future is excellent for retrieving a single piece of data, like fetching a user profile from a database. You make the request, wait, and get one response back.

However, modern applications frequently deal with data that updates continuously. Think of a live chat application, a GPS tracker updating your location, or a live stock market ticker. A single future cannot handle this continuous flow of data. For these scenarios, Dart provides the Stream.

Dart futures versus Dart streams

The easiest way to understand the difference is through a real-world analogy:

  • Future: Ordering a cup of coffee. You place your order, wait, and eventually receive exactly one coffee. The transaction is then complete.

  • Stream: Signing up for a video streaming subscription. You subscribe once, and you continuously receive new movies and shows over time until you cancel the subscription.

A stream is essentially a pipe. Data is pushed into one end of the pipe, and your application listens at the other end, reacting every time a new piece of data emerges.

Generating a stream

To create a function that returns a stream, we use the async* (async star) keyword. The asterisk tells the compiler that this is a generator function—it will generate a series of values rather than just returning one.

Inside an async* function, we do not use the return keyword to output data, because return permanently terminates a function. Instead, we use the yield keyword. yield pushes a single value out of the stream and allows the function to keep running so it can push more values later.

Consuming a stream

To listen to a stream and unpack its values as they arrive, we use an await for loop. This combines the asynchronous pausing of await with the continuous listening of a standard for loop.

Let us build a simple countdown timer to see how we generate and consume a stream in practice.

Dart
Stream<int> countdown(int from) async* {
for (int i = from; i > 0; i--) {
await Future.delayed(Duration(seconds: 1));
yield i;
}
}
void main() async {
print('Starting countdown...');
await for (final number in countdown(3)) {
print(number);
}
print('Liftoff!');
}
  • Line 1: We define our function to return a Stream<int> and mark it with async* so it can emit multiple values.

  • Lines 3—4: Inside our loop, we pause execution for one second. After the delay, we use yield to push the current integer out into the stream. The loop then continues to the next iteration.

  • Line 8: We mark the main execution function as async so we can consume the stream.

  • Lines 11—13: We use the await for loop to subscribe to our countdown stream. The loop pauses at this point, waits for a value to be yielded, prints it, and then goes back to waiting. It will automatically exit the loop once the stream finishes sending data.

  • Line 15: Once the stream is completely exhausted, the application breaks out of the loop and executes the final print statement.

We expanded our asynchronous toolkit by introducing streams. While futures are perfect for one-time asynchronous operations, streams allow us to process a continuous flow of data over time. By combining the async* generator, the yield keyword, and the await for loop, we can easily build applications that react to live data updates, laying the final piece of groundwork required for responsive Flutter development.