Dart: Creating Streams From Futures

A Future in Dart is a way to handle a value that will be available later. It represents a task that takes time to finish, like loading data or waiting for a result.

Sometimes, you want to turn this single future value into a stream — a sequence of events that you can listen to. Creating streams from futures helps you work with asynchronous data in a consistent way, especially when you need to connect or combine streams in your program.

Using Stream.fromFuture()

You can create a stream from a single future using Stream.fromFuture(). This stream will send the future’s result once it completes.

Here’s an example that creates a stream from a future returning a greeting message:

Stream<String> greetStream() => Stream.fromFuture(Future.value('Hello, Dart!'));

void main() {

  greetStream().listen((message) {
    print(message);
  });

}

This stream sends the greeting as a single event when the future finishes.

Using Future.asStream()

Every future has a method called asStream() that turns the future into a stream. This is another way to create a stream from a single future.

Here’s an example using asStream() to convert a future greeting into a stream:

void main() {

  Future<String> futureGreeting = Future.value('Hello from asStream!');

  futureGreeting.asStream().listen((message) {
    print(message);
  });

}

This stream sends the future’s result as a single event when the future completes.

Listening to a Stream From a Future

Once you create a stream from a future, you can listen to it just like any other stream. When the future completes, the stream sends its result to your listener.

Here’s an example that listens to a stream from a future and prints the greeting message:

Stream<String> greetStream() => Stream.fromFuture(Future.value('Hello, Dart!'));

void main() {

  greetStream().listen((message) => print(message));

}

The listener waits for the future to finish, then receives and prints the value from the stream.

Creating Streams from Delayed Futures

You can use Future.delayed to create a future that completes after some time. Then, use Stream.fromFuture() to turn it into a stream. This is useful when you want to simulate waiting for something—like a loading screen or a timed message.

Here’s an example that streams a cheer message after a short delay:

Stream<String> delayedCheer() => Stream.fromFuture(
  Future.delayed(Duration(seconds: 2), () => 'Go team!'),
);

void main() {

  delayedCheer().listen((cheer) {
    print(cheer);
  });

}

This shows how a future with a delay becomes a stream that sends its result once it’s ready.

Using Futures Returning Custom Objects

You can also create a stream from a future that returns a custom object. This is helpful when working with real-world data like users, animals, or products.

Here’s an example where a future returns an Animal object, and we stream it using Stream.fromFuture():

class Animal {

  final String name;

  Animal(this.name);

}

Stream<Animal> animalStream() => Stream.fromFuture(
  Future.value(Animal('Leo')),
);

void main() {

  animalStream().listen((animal) {
    print('Animal: ${animal.name}');
  });

}

The stream sends the custom object when the future completes, just like it would with a basic value.

Using async* to Stream From Multiple Futures

When you have more than one future and want to send each result one after the other, use an async* function with yield. This lets you build a stream that gives out values as each future completes.

Here’s an example that streams two delayed messages:

Stream<String> multipleMessages() async* {

  yield await Future.delayed(Duration(seconds: 1), () => 'Hello');
  yield await Future.delayed(Duration(seconds: 1), () => 'World');

}

void main() {

  multipleMessages().listen((message) {
    print(message);
  });

}

This approach gives you more control, letting you send values from multiple futures one at a time.

Converting Future Errors to Stream Errors

If a future throws an error, and you convert it into a stream using Stream.fromFuture(), the stream will send that error. This helps you handle failures in a consistent stream-based way.

Here’s an example where a future throws an error, and the stream reports it:

Stream<String> errorStream() => Stream.fromFuture(
  Future.error('Oops, something went wrong!'),
);

void main() {

  errorStream().listen(
    (data) => print('Data: $data'),
    onError: (err) => print('Error: $err'),
    onDone: () => print('Done'),
  );

}

This shows how future failures become stream errors that you can catch using the onError callback.

Conclusion

In Dart, you can easily create streams from futures using Stream.fromFuture() or Future.asStream(). Whether the future gives a simple value, a delayed result, or a custom object, you can turn it into a stream and listen to it like any other.

You can also use async* with yield to stream values from multiple futures one by one. And if a future throws an error, the stream will pass that error along, allowing you to handle it safely.

These tools help you connect futures and streams smoothly in your Dart programs.