flutter
/

Dart Stream – Asynchronous Data Sequences

Last Sync: Today

On this page

12
0%
5 min read
Remaining
5 minleft

Click any section to jump — progress syncs automatically

flutter

Dart Stream – Asynchronous Data Sequences

What is a Stream?

A Stream is a sequence of asynchronous events. It delivers a series of data events (values) and can optionally complete with a done event or an error. Unlike a Future, which delivers a single value, a stream can deliver multiple values over time. Streams are essential for handling data that arrives over time, such as user input, file I/O, network responses, or timers.

Types of Streams

Dart has two kinds of streams:

    • Single‑subscription streams – allow only one listener during the stream's lifetime. They are used for sequences that are consumed once (e.g., reading a file).
    • Broadcast streams – allow any number of listeners. Listeners can subscribe at any time, and they receive events that are fired after they subscribe. Broadcast streams are used for events like button clicks.

Creating a Stream

There are several ways to create a stream in Dart:

    • Using an async* function with yield.
    • Using a StreamController for manual control.
    • From an iterable with Stream.fromIterable().
    • From a Future with Stream.fromFuture() or Stream.fromFutures().
    • Using periodic timers with Stream.periodic().

Creating a Stream with async*

DARTRead-only
1
Stream<int> countStream(int max) async* {
  for (int i = 1; i <= max; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i; // Emit a value
  }
}

void main() async {
  await for (int value in countStream(5)) {
    print(value); // prints 1,2,3,4,5 each second
  }
}

Creating a Stream with StreamController

DARTRead-only
1
import 'dart:async';

void main() {
  final controller = StreamController<int>();

  // Add data to the stream
  controller.sink.add(1);
  controller.sink.add(2);
  controller.sink.add(3);
  controller.close();

  // Listen to the stream
  controller.stream.listen(
    (data) => print('Data: $data'),
    onError: (error) => print('Error: $error'),
    onDone: () => print('Done'),
  );
}

Listening to a Stream

The primary way to consume a stream is by using the listen() method. It returns a StreamSubscription which you can use to control the subscription (pause, resume, cancel).

DARTRead-only
1
void main() {
  var stream = Stream.periodic(Duration(seconds: 1), (i) => i).take(5);

  var subscription = stream.listen(
    (data) => print('Data: $data'),
    onError: (err) => print('Error: $err'),
    onDone: () => print('Done'),
    cancelOnError: false, // whether to cancel on first error
  );

  // Later, if needed
  // subscription.pause();
  // subscription.resume();
  // subscription.cancel();
}

Using await for

In an async function, you can use await for to iterate over stream events as they arrive. The loop ends when the stream closes.

DARTRead-only
1
Future<void> printStream(Stream<int> stream) async {
  await for (int value in stream) {
    print(value);
  }
}

void main() async {
  var stream = Stream.periodic(Duration(seconds: 1), (i) => i).take(5);
  await printStream(stream);
}

Transforming Streams

Streams provide a set of methods similar to those on iterables, allowing you to transform the data:

    • map – applies a function to each event.
    • where – filters events based on a predicate.
    • expand – expands each event into multiple events.
    • take / skip – limits the number of events.
    • distinct – removes consecutive duplicates.
    • timeout – raises an error if no event arrives within a duration.
DARTRead-only
1
void main() async {
  var stream = Stream.fromIterable([1, 2, 3, 4, 5]);

  var doubled = stream.map((x) => x * 2);
  var even = stream.where((x) => x % 2 == 0);

  await for (int value in doubled) {
    print('Doubled: $value');
  }

  await for (int value in even) {
    print('Even: $value');
  }
}

Handling Errors

Errors in a stream can be handled using onError callback in listen, or using handleError transformer. If unhandled, errors will terminate the stream.

DARTRead-only
1
void main() {
  var controller = StreamController<int>();
  controller.add(1);
  controller.addError('Something went wrong');
  controller.add(2);
  controller.close();

  controller.stream
    .handleError((error) {
      print('Caught error: $error');
      // Return a fallback value, rethrow, etc.
    })
    .listen(print);
}

Broadcast Streams

By default, streams are single‑subscription. To create a broadcast stream, use asBroadcastStream() on a single‑subscription stream, or create a StreamController.broadcast().

DARTRead-only
1
void main() {
  var controller = StreamController<int>.broadcast();

  controller.stream.listen((data) => print('Listener 1: $data'));
  controller.stream.listen((data) => print('Listener 2: $data'));

  controller.add(1);
  controller.add(2);
  controller.close();
}

Common Patterns

    • Merging streams – use StreamGroup.merge or StreamZip.
    • Debouncing – use debounce (from package:rxdart or custom).
    • Combining latest values – use Rx.combineLatest2 (from rxdart).

Key Takeaways

    • A Stream is a sequence of asynchronous events.
    • Use listen() or await for to consume stream events.
    • Streams can be single‑subscription or broadcast.
    • Use StreamController for manual control, or async* functions for generator‑style streams.
    • Transform streams using methods like map, where, take, etc.
    • Always handle errors to avoid uncaught exceptions.

Try it yourself

import 'dart:async';

void main() {
  // Create a stream that emits numbers every second
  var stream = Stream.periodic(Duration(seconds: 1), (i) => i).take(5);

  stream.listen(
    (data) => print('Data: $data'),
    onDone: () => print('Done'),
  );
}

Test Your Knowledge

Q1
of 4

What does a Stream represent?

A
A single value that will be available later
B
A sequence of asynchronous events
C
A synchronous collection
D
An error handler
Q2
of 4

Which method is used to listen to a stream?

A
then()
B
listen()
C
await
D
forEach()
Q3
of 4

What type of stream allows multiple listeners?

A
Single‑subscription stream
B
Broadcast stream
C
Both
D
Neither
Q4
of 4

How do you create a stream that emits values using a generator function?

A
async function
B
async* function
C
Stream.periodic
D
Stream.fromIterable

Frequently Asked Questions

What is the difference between a Future and a Stream?

A Future represents a single value (or error) that will be available later. A Stream represents multiple values (or errors) delivered over time. Use Future for one‑shot async operations, and Stream for sequences of events.

What is a single‑subscription stream?

A single‑subscription stream allows only one listener during its lifetime. It's ideal for consuming a sequence once, like reading a file. If you try to listen again, you'll get an error.

What is a broadcast stream?

A broadcast stream allows any number of listeners at any time. Listeners added after events are emitted won't see those past events. Broadcast streams are suitable for events like UI interactions.

How do I create a stream that emits values periodically?

Use Stream.periodic(Duration(seconds: 1), (i) => i).

How do I convert a Stream to a Future?

Use methods like first, last, single, drain, or toList() on the stream. For example: var firstValue = await stream.first;.

Can I reuse a stream after it's been listened to?

For single‑subscription streams, no – you must create a new stream. For broadcast streams, yes, you can add multiple listeners at any time.

What happens if I don't handle errors in a stream?

Unhandled errors may cause the stream to stop and the error to propagate to the zone, possibly crashing the app. Always provide an onError callback or use handleError.

Previous

dart future

Next

dart isolates

Related Content

Need help?

Explore our comprehensive docs or start a chat with our tech experts.