DEV Community

Cover image for Stream vs Future in Dart: Stop Using the Wrong One
Md. Al-Amin
Md. Al-Amin

Posted on

Stream vs Future in Dart: Stop Using the Wrong One

When building Flutter apps, you’ve probably asked yourself:

“Should I use a Future here? Or do I need a Stream?”

Many developers mix these up, which leads to performance issues, hard-to-maintain code, or UI bugs. In this blog, we’ll unpack when to use Future, when to use Stream, and how to avoid the most common mistakes with examples and real-world use cases.


What’s the Difference?

Let’s quickly define the two:

Future

  • Represents a single asynchronous value.
  • Completes once.
  • Best for short-lived operations.

Example: Fetching a user profile from a server.

Stream

  • Represents a sequence of asynchronous values over time.
  • Can emit multiple values.
  • May never complete.
  • Ideal for long-lived or real-time data.

Example: Receiving real-time messages from a chat app.


Future Example

Future<String> fetchUser() async {
  await Future.delayed(Duration(seconds: 2));
  return 'Alamin Karno';
}
Enter fullscreen mode Exit fullscreen mode

Using it in a FutureBuilder:

FutureBuilder(
  future: fetchUser(),
  builder: (context, snapshot) {
    if (snapshot.connectionState == ConnectionState.waiting) {
      return CircularProgressIndicator();
    }
    return Text('Hello, ${snapshot.data}');
  },
)
Enter fullscreen mode Exit fullscreen mode

Good use cases for Future:

  • Fetching one-time data (e.g. user profile, config)
  • Writing to a local file
  • Showing a splash delay
  • Getting device permissions or version info

Stream Example

Stream<int> counterStream() async* {
  for (int i = 0; i < 5; i++) {
    await Future.delayed(Duration(seconds: 1));
    yield i;
  }
}
Enter fullscreen mode Exit fullscreen mode

Using it in a StreamBuilder:

StreamBuilder<int>(
  stream: counterStream(),
  builder: (context, snapshot) {
    if (!snapshot.hasData) return CircularProgressIndicator();
    return Text('Counter: ${snapshot.data}');
  },
)
Enter fullscreen mode Exit fullscreen mode

Good use cases for Stream:

  • Listening to Firebase/FireStore changes
  • Real-time chat messages
  • GPS or sensor updates
  • Bluetooth data streams
  • Internet connectivity updates

Common Mistakes to Avoid

Using FutureBuilder for continuous data
Using FutureBuilder with something that emits data repeatedly (like Firebase or a location stream) is a bad idea. FutureBuilder runs once and won’t update when new data comes.

Use StreamBuilder for such cases.


Forgetting to cancel a stream subscription
If you manually use listen() on a stream, don’t forget to cancel it in dispose():

late StreamSubscription subscription;

@override
void initState() {
  subscription = someStream.listen((event) {
    // handle event
  });
  super.initState();
}

@override
void dispose() {
  subscription.cancel();
  super.dispose();
}
Enter fullscreen mode Exit fullscreen mode

Using Stream.fromFuture() without reason
This is unnecessary unless you’re working with an API that only takes a stream. Don’t turn a Future into a Stream unless the use case requires it.


Real-World Use Cases (In Words)

  • User login: Use Future — it’s a one-time action that returns a result.
  • Firebase Firestore updates: Use Stream — documents can change any time.
  • Location tracking: Use Stream — location changes over time.
  • Get device info or permissions: Use Future.
  • Live sensor data from a fitness app: Use Stream.

Advanced Tips

Creating your own Stream

Stream<int> generateStream() async* {
  yield 1;
  await Future.delayed(Duration(seconds: 1));
  yield 2;
}
Enter fullscreen mode Exit fullscreen mode

Combine multiple streams using RxDart:

Rx.combineLatest2(stream1, stream2, (a, b) => '$a & $b');
Enter fullscreen mode Exit fullscreen mode

Convert stream to future if you only need the first value:

final result = await myStream.first;
Enter fullscreen mode Exit fullscreen mode

Best Practices

  • Use FutureBuilder for short, one-time data needs.
  • Use StreamBuilder for long running or changing data.
  • Always dispose of stream subscriptions if you manually listen to them.
  • Avoid Stream.periodic() unless absolutely needed.
  • For complex use cases, use rxdart for reactive patterns like debouncing, combining streams, etc.

Conclusion

Choosing between Future and Stream is not just about personal preference it’s about picking the right tool for the job.

  • Use Future for short, one-time tasks.
  • Use Stream when data changes over time.

Mastering this will help you write faster, cleaner, and more reactive Flutter apps and avoid many hidden bugs in your UI or business logic.

Top comments (0)