When building Flutter apps, you’ve probably asked yourself:
“Should I use a
Futurehere? Or do I need aStream?”
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';
}
Using it in a FutureBuilder:
FutureBuilder(
future: fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
}
return Text('Hello, ${snapshot.data}');
},
)
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;
}
}
Using it in a StreamBuilder:
StreamBuilder<int>(
stream: counterStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return Text('Counter: ${snapshot.data}');
},
)
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();
}
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;
}
Combine multiple streams using RxDart:
Rx.combineLatest2(stream1, stream2, (a, b) => '$a & $b');
Convert stream to future if you only need the first value:
final result = await myStream.first;
Best Practices
- Use
FutureBuilderfor short, one-time data needs. - Use
StreamBuilderfor 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
rxdartfor 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
Futurefor short, one-time tasks. - Use
Streamwhen 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)