Asynchronous programming is a core part of Dart and Flutter development. Whether you are calling an API, reading data from a database, listening to authentication changes, tracking location updates, or receiving real-time messages, you will frequently work with two important Dart types:
Future<T>
and:
Stream<T>
Both represent asynchronous operations, but they solve different problems.
The simplest difference is:
A Future represents a single asynchronous result, while a Stream represents a sequence of asynchronous events over time.
For example:
Future
Request ───────────────► One Result
Stream
Subscribe ──► Data ──► Data ──► Data ──► Data...
However, there is much more to understand than this basic definition.
Future vs Stream in Dart: Quick Comparison
| Feature | Future | Stream |
|---|---|---|
| Results | Usually one result | Zero, one, or many events |
| Duration | Completes once | Can continue over time |
| Common syntax | async / await | async* / yield |
| Consumer | await, then() | listen(), await for |
| Flutter widget | FutureBuilder | StreamBuilder |
| Cancellation | No general cancellation on Future itself | Subscription can be cancelled |
| Typical API call | Yes | Usually not needed |
| Real-time updates | Not by itself | Yes |
| Firestore live snapshots | No | Yes |
| Authentication state changes | No | Yes |
| One database query | Yes | Depends on database API |
| Continuous sensor data | No | Yes |
A useful mental model is:
Need one result?
→ Future
Need values/events over time?
→ Stream
What Is a Future in Dart?
A Future represents a value or error that may become available later.
For example:
Future<String> getUsername() async {
await Future.delayed(
const Duration(seconds: 2),
);
return 'Rahul';
}
Calling:
final username = await getUsername();
does not immediately produce the final string.
Conceptually:
Call Function
↓
Future<String>
↓
Wait
↓
String Result
Eventually the future completes.
A Future Completes Once
This is one of the defining characteristics of a Future.
Consider:
Future<int> getUserCount() async {
return 100;
}
The operation produces:
100
and completes.
It does not later produce:
101
102
103
104
through that same Future.
Conceptually:
Future
Start
│
│
▼
Result
│
▼
Complete
Once completed, that asynchronous operation is finished.
Basic Future Example
Future<String> fetchUser() async {
await Future.delayed(
const Duration(seconds: 2),
);
return 'Ankit';
}
Use:
Future<void> loadUser() async {
final user = await fetchUser();
print(user);
}
Output after approximately two seconds:
Ankit
This is a perfect Future use case because we expect one final result.
Real Example: API Request with Future
Most normal HTTP requests naturally return Futures.
For example:
Future<List<User>> fetchUsers() async {
final response = await http.get(
Uri.parse(
'https://example.com/users',
),
);
if (response.statusCode != 200) {
throw Exception(
'Failed to load users',
);
}
final decoded =
jsonDecode(response.body)
as List<dynamic>;
return decoded
.map(
(json) => User.fromJson(
json as Map<String, dynamic>,
),
)
.toList();
}
The flow is:
Request Users
↓
Wait for Server
↓
Receive Response
↓
Parse Data
↓
List<User>
↓
Future Completes
You requested the data once and received one final response.
Therefore:
Future<List<User>>
is appropriate.
What Is a Stream in Dart?
A Stream represents a sequence of asynchronous events.
For example:
Stream<int> countNumbers() async* {
for (var i = 1; i <= 5; i++) {
await Future.delayed(
const Duration(seconds: 1),
);
yield i;
}
}
This does not return just one integer.
Instead, it emits:
1
↓
2
↓
3
↓
4
↓
5
over time.
Basic Stream Example
Stream<int> numberStream() async* {
yield 1;
yield 2;
yield 3;
}
Listen:
numberStream().listen(
(number) {
print(number);
},
);
Output:
1
2
3
The important difference is that the listener can receive multiple events from one stream.
Future vs Stream Visually
A Future looks like:
Start
│
│
│
▼
Result
│
▼
Done
A Stream looks like:
Start Listening
│
▼
Event 1
│
▼
Event 2
│
▼
Event 3
│
▼
Event 4
│
▼
...
A stream may eventually close, or it may remain active for a long time depending on its source.
Future Uses async
A Future-producing function commonly uses:
async
Example:
Future<String> loadName() async {
await Future.delayed(
const Duration(seconds: 1),
);
return 'Flutter';
}
Notice:
return 'Flutter';
The function eventually produces one result.
Stream Uses async*
A stream-producing asynchronous generator can use:
async*
Example:
Stream<int> generateNumbers() async* {
for (var i = 1; i <= 5; i++) {
await Future.delayed(
const Duration(seconds: 1),
);
yield i;
}
}
The important keyword here is:
yield
Instead of returning the final value immediately, yield adds an event to the stream.
return vs yield
This difference is useful to remember.
Future:
Future<int> getNumber() async {
return 10;
}
Conceptually:
return
→ Final result
Stream:
Stream<int> getNumbers() async* {
yield 10;
yield 20;
yield 30;
}
Conceptually:
yield
→ Emit event
yield
→ Emit another event
yield
→ Emit another event
Therefore:
Future
→ return
Stream generator
→ yield
Consuming a Future with await
The most common way to consume a Future is:
final result = await getData();
Example:
Future<void> loadData() async {
final name = await getUsername();
print(name);
}
Execution pauses within that asynchronous function until the Future completes, while Dart’s event loop can continue handling other work.
Consuming a Future with then()
You can also write:
getUsername().then(
(username) {
print(username);
},
);
Both styles can work.
However, async/await is often easier to read for sequential asynchronous logic.
For example:
final user = await getUser();
final orders = await getOrders(user.id);
final profile = await getProfile(user.id);
is often easier to follow than deeply nested callbacks.
Consuming a Stream with listen()
The common way to subscribe to a Stream is:
final subscription =
numberStream().listen(
(value) {
print(value);
},
);
Every time the stream emits an event:
yield value;
the listener receives it.
Conceptually:
Stream
│
├── Event 1 → Listener
├── Event 2 → Listener
├── Event 3 → Listener
└── Event 4 → Listener
Consuming a Stream with await for
Dart also supports:
await for
Example:
Future<void> readNumbers() async {
await for (
final number in numberStream()
) {
print(number);
}
}
This can make asynchronous stream processing easier to read.
Conceptually:
Wait for Event
↓
Process Event
↓
Wait for Next Event
↓
Process Event
↓
...
The loop finishes when the stream closes, unless it exits earlier or an error interrupts processing.
Real Example: Countdown Stream
Stream<int> countdown(
int start,
) async* {
for (
var number = start;
number >= 0;
number--
) {
yield number;
await Future.delayed(
const Duration(seconds: 1),
);
}
}
Use:
countdown(5).listen(
(number) {
print(number);
},
);
Output over time:
5
4
3
2
1
0
A Future would not naturally represent this sequence because multiple values arrive over time.
Real Flutter Example: FutureBuilder
Flutter provides:
FutureBuilder
for building UI from a Future.
Example:
FutureBuilder<User>(
future: fetchUser(),
builder: (
context,
snapshot,
) {
if (snapshot.connectionState ==
ConnectionState.waiting) {
return const Center(
child:
CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}',
);
}
if (!snapshot.hasData) {
return const Text(
'No user found',
);
}
final user = snapshot.data!;
return Text(user.name);
},
)
The lifecycle is roughly:
FutureBuilder
↓
Waiting
↓
Future completes
↓
Data / Error
↓
UI updates
This works well for one-time asynchronous data.
Important FutureBuilder Mistake
Avoid repeatedly creating the Future directly during every build when that is not intentional.
For example:
FutureBuilder<User>(
future: fetchUser(),
builder: ...,
)
If the parent rebuilds and a new Future is created, the asynchronous operation may run again.
A better pattern for a one-time request in a StatefulWidget is:
late Future<User> userFuture;
@override
void initState() {
super.initState();
userFuture = fetchUser();
}
Then:
FutureBuilder<User>(
future: userFuture,
builder: ...,
)
Now unrelated rebuilds do not automatically create a new API request.
Real Flutter Example: StreamBuilder
For continuously changing data, Flutter provides:
StreamBuilder
Example:
StreamBuilder<int>(
stream: countdown(10),
builder: (
context,
snapshot,
) {
if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}',
);
}
if (!snapshot.hasData) {
return const
CircularProgressIndicator();
}
return Text(
'${snapshot.data}',
);
},
)
Every new stream event can cause the builder to receive a new snapshot.
Conceptually:
Stream emits 10
→ UI shows 10
Stream emits 9
→ UI shows 9
Stream emits 8
→ UI shows 8
...
This is fundamentally different from a FutureBuilder, which usually waits for one completion.
Real Example: Firebase Firestore
Firestore demonstrates the difference extremely well.
Suppose you want to fetch documents once.
Depending on the Firebase API, you might perform a one-time request:
Future<QuerySnapshot<Map<String, dynamic>>>
getUsers() {
return FirebaseFirestore.instance
.collection('users')
.get();
}
The flow is:
Request
↓
Current Data
↓
Future completes
Now suppose you want the UI to update when the Firestore collection changes.
You can use:
Stream<QuerySnapshot<Map<String, dynamic>>>
watchUsers() {
return FirebaseFirestore.instance
.collection('users')
.snapshots();
}
Now:
Initial Data
↓
Database Changes
↓
New Event
↓
Database Changes
↓
New Event
↓
...
That is exactly what Streams are designed for.
Firestore with StreamBuilder
StreamBuilder<
QuerySnapshot<Map<String, dynamic>>>(
stream: FirebaseFirestore.instance
.collection('users')
.snapshots(),
builder: (
context,
snapshot,
) {
if (snapshot.hasError) {
return Text(
'Error: ${snapshot.error}',
);
}
if (!snapshot.hasData) {
return const
CircularProgressIndicator();
}
final docs =
snapshot.data!.docs;
return ListView.builder(
itemCount: docs.length,
itemBuilder: (
context,
index,
) {
final data =
docs[index].data();
return ListTile(
title: Text(
data['name'] ?? '',
),
);
},
);
},
)
When Firestore emits a new snapshot, the widget receives updated data.
Authentication Example
Authentication state is another natural Stream use case.
A user can:
Logged Out
↓
Login
↓
Logged In
↓
Logout
↓
Logged Out
These are multiple events over time.
Firebase Authentication exposes a stream for this type of state:
FirebaseAuth.instance
.authStateChanges();
Conceptually:
Stream<User?>
null
↓
User
↓
null
↓
User
A Future would not naturally represent ongoing authentication changes.
Future API vs Real-Time API
Imagine a chat application.
You could fetch messages once:
Future<List<Message>>
getMessages() async {
// Fetch existing messages.
}
This gives:
Messages at request time
↓
Done
If another user sends a new message later, that original Future does not suddenly emit another value.
For real-time chat, a Stream is a better abstraction:
Stream<List<Message>>
watchMessages() {
// Listen for message changes.
}
Now:
Messages
↓
New message
↓
Updated messages
↓
Another message
↓
Updated messages
StreamSubscription
When using:
stream.listen(...)
Dart returns a:
StreamSubscription<T>
Example:
final subscription =
numberStream().listen(
(number) {
print(number);
},
);
The subscription gives you control over the listener.
Pause a Stream Subscription
You can pause it:
subscription.pause();
Later:
subscription.resume();
This can be useful when a consumer temporarily does not want events delivered normally.
Cancel a Stream Subscription
You can cancel:
await subscription.cancel();
After cancellation, that subscription no longer receives events.
This lifecycle control is an important difference between streams and ordinary Futures.
Future Cancellation vs Stream Cancellation
A standard Dart Future does not expose a general-purpose:
future.cancel();
method.
Once an asynchronous operation has started, cancellation depends on the underlying API or on designing explicit cancellation behavior.
Streams are different.
A StreamSubscription provides:
subscription.cancel();
Therefore, streams naturally support subscription lifecycle management.
Error Handling with Future
With async/await:
try {
final user =
await fetchUser();
print(user.name);
} catch (error) {
print(
'Error: $error',
);
}
Or:
fetchUser()
.then((user) {
print(user.name);
})
.catchError((error) {
print(error);
});
A Future completes with either:
Value
or:
Error
Error Handling with Stream
Streams can emit errors while they are active.
For example:
stream.listen(
(data) {
print(data);
},
onError: (error) {
print(
'Stream error: $error',
);
},
onDone: () {
print(
'Stream closed',
);
},
);
A stream can conceptually produce:
Data
↓
Data
↓
Error
↓
Data
↓
Done
depending on how the stream and listener are configured.
Streams therefore have a richer event lifecycle than a single Future.
Stream Events
A stream can communicate:
Data Event
Error Event
Done Event
A listener can respond to each:
stream.listen(
(data) {
// Data event
},
onError: (error) {
// Error event
},
onDone: () {
// Stream closed
},
);
This makes streams suitable for long-running asynchronous event sources.
Single-Subscription Streams
By default, many streams are designed for one listener.
Conceptually:
Stream
│
└── Listener
Trying to listen multiple times to a single-subscription stream can produce an error.
These streams are appropriate when events form one sequence that should be consumed by one listener.
Examples can include:
File reading
Socket-like data sequences
Generated async sequences
depending on the API.
Broadcast Streams
Broadcast streams allow multiple listeners.
Conceptually:
┌── Listener A
Stream ────────┼── Listener B
└── Listener C
You can create one using:
final controller =
StreamController<int>.broadcast();
Then multiple listeners can subscribe:
controller.stream.listen(
(value) {
print(
'Listener A: $value',
);
},
);
controller.stream.listen(
(value) {
print(
'Listener B: $value',
);
},
);
Add:
controller.add(10);
Both active listeners can receive the event.
StreamController
Sometimes you need to create and control your own stream.
Dart provides:
StreamController<T>
Example:
final controller =
StreamController<String>();
Add events:
controller.add(
'Hello',
);
controller.add(
'Flutter',
);
Listen:
controller.stream.listen(
(value) {
print(value);
},
);
Finally:
await controller.close();
Output:
Hello
Flutter
Real Example: Download Progress
Imagine downloading a large file.
Returning only a Future could give:
Start Download
↓
Wait
↓
File Downloaded
But what if the UI needs:
10%
20%
30%
40%
...
100%
A Stream can model these progress events naturally:
Stream<int> downloadProgress() async* {
for (
var progress = 0;
progress <= 100;
progress += 10
) {
await Future.delayed(
const Duration(
milliseconds: 300,
),
);
yield progress;
}
}
The UI can react to every value.
Real Example: Search Suggestions
Suppose a user types:
f
fl
flu
flut
flutt
flutter
Each text change is an event.
Conceptually:
Text Input
↓
Stream
↓
Debounce
↓
Search
↓
Suggestions
Streams can be useful for this event-driven workflow.
However, each individual HTTP search request may still return a Future.
This means Futures and Streams often work together rather than competing.
Future and Stream Can Work Together
Consider:
Search Input Stream
↓
User types query
↓
HTTP request
↓
Future<SearchResult>
↓
Result displayed
The changing query can be represented by a Stream.
Each API request can return a Future.
Therefore:
Future vs Stream is not always about choosing only one. Real applications frequently use both.
Converting a Future to a Stream
Dart allows:
final stream =
myFuture.asStream();
Example:
final future =
Future.value(100);
final stream =
future.asStream();
The stream emits the Future’s result and then closes.
Conceptually:
Future
↓
100
↓
Complete
Converted Stream
↓
100
↓
Done
This does not magically turn a one-time data source into a real-time source.
Getting a Future from a Stream
Streams also provide operations that return Futures.
For example:
final firstValue =
await stream.first;
or:
final lastValue =
await stream.last;
or:
final values =
await stream.toList();
toList() waits until the stream finishes, then returns:
Future<List<T>>
This demonstrates how Future and Stream abstractions can interact.
Transforming Streams
Streams provide useful operators such as:
stream.map(...)
Example:
final doubled =
numberStream().map(
(number) => number * 2,
);
If the source emits:
1
2
3
the transformed stream emits:
2
4
6
Filtering Streams
Use:
stream.where(...)
Example:
final evenNumbers =
numberStream().where(
(number) => number.isEven,
);
If the source emits:
1
2
3
4
5
the result is:
2
4
Stream asyncMap()
Sometimes every stream event needs asynchronous processing.
Use:
stream.asyncMap(...)
For example:
final users =
userIds.asyncMap(
(id) async {
return fetchUser(id);
},
);
Here:
Stream of IDs
↓
Async operation
↓
Stream of Users
This is another example of Streams and Futures working together.
Performance: Is Future Faster Than Stream?
This is usually the wrong question.
Future and Stream represent different asynchronous shapes.
Use Future when you need one result.
Use Stream when you need a sequence of events.
Creating a Stream for a one-time value can introduce unnecessary complexity.
Likewise, repeatedly polling with Futures may be awkward when the underlying API already provides a proper event stream.
Choose the abstraction based on the data lifecycle first.
Future Does Not Mean Another Isolate
Another common misconception is:
Future(() {
heavyCalculation();
});
means the calculation runs on another isolate.
It does not.
A Future represents asynchronous completion, but normal Future scheduling does not automatically move CPU-intensive Dart code to another isolate.
For CPU-heavy work, consider isolate APIs when appropriate:
final result =
await Isolate.run(
heavyCalculation,
);
Therefore:
Future
→ Asynchronous result
Stream
→ Asynchronous event sequence
Isolate
→ Independent Dart execution context
These concepts solve different problems.
Common Future Use Cases
Use a Future for operations such as:
HTTP request
One-time database query
Read one file
Save settings
Login request
Register user
Upload completion
Fetch profile
Load configuration
Request permission
Get current value once
For example:
Future<User> login() async {
// Login request
}
The operation finishes with one result or an error.
Common Stream Use Cases
Use a Stream for continuously changing data such as:
Authentication changes
Firestore snapshots
Chat messages
WebSocket events
Sensor readings
Location updates
Connectivity events
Progress updates
Timer-like event sequences
Database watchers
User-generated event pipelines
For example:
Stream<List<Message>>
watchMessages() {
// Real-time messages
}
The data can continue changing after the initial event.
FutureBuilder vs StreamBuilder
This is one of the most useful Flutter comparisons.
FutureBuilder
Use:
FutureBuilder<T>
when the data source returns:
Future<T>
Typical flow:
Loading
↓
Data / Error
↓
Completed
Examples:
Load profile
Fetch products
Check configuration
One-time database request
StreamBuilder
Use:
StreamBuilder<T>
when the source returns:
Stream<T>
Typical flow:
Waiting
↓
Data
↓
New Data
↓
New Data
↓
...
Examples:
Chat
Firestore live data
Authentication state
Real-time tracking
Live dashboard
Future vs Stream in Repository Architecture
Consider a user repository:
abstract class UserRepository {
Future<User> getUser(
String id,
);
Stream<User> watchUser(
String id,
);
}
These methods may look similar, but their meaning is different.
getUser(id)
means:
Give me the user once.
While:
watchUser(id)
means:
Keep giving me updated user data while I am subscribed.
That naming distinction can make large Flutter projects much easier to understand.
Practical Service Example
class UserService {
Future<User> fetchUser(
String id,
) async {
// Fetch once.
throw UnimplementedError();
}
Stream<User> watchUser(
String id,
) {
// Listen continuously.
throw UnimplementedError();
}
}
Clear method names such as:
fetch
get
load
often communicate one-time operations.
Names such as:
watch
listen
observe
can communicate ongoing streams.
Consistency makes asynchronous APIs easier to understand.
Common Mistake: Using Stream for Everything
A Stream is not automatically better because it supports multiple events.
Suppose you simply need:
User opens profile
↓
Fetch profile once
↓
Display profile
A Future may be simpler:
Future<User> fetchProfile()
Using a StreamController, subscriptions, and lifecycle management for a single response can create unnecessary complexity.
Common Mistake: Using Future for Real-Time Data
Suppose you write:
Future<List<Message>>
fetchMessages();
Then manually call it every few seconds:
Fetch
↓
Wait 5 seconds
↓
Fetch
↓
Wait
↓
Fetch
Polling can be valid in some systems, but if the backend already supports real-time events, a Stream may provide a cleaner abstraction.
For example:
Stream<List<Message>>
watchMessages();
Common Mistake: Forgetting Stream Cleanup
When manually subscribing:
late StreamSubscription<int>
subscription;
Start:
subscription =
stream.listen(
(value) {
// Handle event
},
);
When the listener is no longer needed:
@override
void dispose() {
subscription.cancel();
super.dispose();
}
Failing to clean up manually managed subscriptions can cause unnecessary work and unwanted callbacks.
Widgets such as StreamBuilder manage their own subscription lifecycle based on the stream you provide, but custom subscriptions remain your responsibility.
Common Mistake: Creating Streams in build()
Just as with Futures, avoid unintentionally creating a new stream source every time build() executes.
For example:
StreamBuilder<User>(
stream: createNewStream(),
builder: ...,
)
may restart work if createNewStream() returns a new stream instance on rebuild.
Prefer obtaining stable streams from your state, controller, repository, or lifecycle initialization when appropriate.
Common Mistake: Confusing Stream with State
A Stream is a sequence of events.
It is not automatically the same thing as application state.
For example:
Stream Event
→ User updated
Your application may consume that event and update state.
Libraries such as Bloc, Riverpod, GetX, or other state-management solutions can manage state around asynchronous data, but the underlying Future/Stream concepts remain the same.
Future vs Stream Decision Guide
Use this simple flow:
Need asynchronous data?
↓
How many results can arrive?
│
├── One
│ ↓
│ Future
│
└── Multiple over time
↓
Stream
Then ask another question:
Does the source genuinely change over time?
│
Yes
↓
Stream
Otherwise:
One request
↓
One response
↓
Future
Real-World Examples
| Requirement | Future or Stream? |
|---|---|
| Fetch products once | Future |
| Login user | Future |
| Register account | Future |
| Upload file and await completion | Future |
| Load profile once | Future |
| Save settings | Future |
| Fetch current weather once | Future |
| Watch authentication state | Stream |
| Firestore live collection | Stream |
| Chat messages | Stream |
| WebSocket messages | Stream |
| GPS updates | Stream |
| Sensor readings | Stream |
| Connectivity changes | Stream |
| Download progress events | Stream |
The key phrase is “over time.”
If multiple meaningful events can arrive over time, a Stream is often a natural abstraction.
Frequently Asked Questions
What is the main difference between Future and Stream in Dart?
A Future represents one eventual completion with a value or error. A Stream represents a sequence of asynchronous data, error, and completion events.
Can a Future return multiple values?
No. A single Future completes once.
If you need multiple values over time, use a Stream.
Can a Stream emit only one value?
Yes.
A Stream can emit zero, one, or many data events. However, if your problem inherently produces only one result, a Future is often the simpler abstraction.
What is async* in Dart?
async* is used to define an asynchronous generator that returns a Stream.
Example:
Stream<int> numbers() async* {
yield 1;
yield 2;
}
What does yield do?
yield emits a value from a generator into its Stream.
Can I use await with a Stream?
Not in the same way as a single Future, but you can use:
await for (
final value in stream
) {
// Handle value
}
You can also await stream operations such as:
await stream.first;
Should an API call return Future or Stream?
A normal one-time HTTP request usually returns a Future.
If the API represents ongoing events, such as a WebSocket connection, a Stream may be more appropriate.
FutureBuilder or StreamBuilder?
Use FutureBuilder for a Future and StreamBuilder for a Stream.
Is Firebase Firestore a Future or Stream?
It can provide both patterns depending on the operation. A one-time read returns an asynchronous one-time result, while snapshots() provides a Stream of updates.
Can Streams be cancelled?
A StreamSubscription can be cancelled using:
await subscription.cancel();
Can a Future be cancelled?
A normal Dart Future does not expose a universal cancel() method. Cancellation must be supported by the underlying operation or implemented through another abstraction.
Does Future run code in the background?
A Future does not automatically mean another isolate. CPU-heavy synchronous Dart code still requires appropriate isolate usage if it needs to execute independently from the main isolate.
Final Thoughts
The difference between Future and Stream becomes straightforward once you focus on the number and timing of results.
A Future represents:
Start Operation
↓
Wait
↓
One Result / Error
↓
Complete
A Stream represents:
Start Listening
↓
Event
↓
Event
↓
Event
↓
...
↓
Done
Therefore, use:
Future<T>
when you need one eventual result, such as:
API response
Login result
Profile data
File read
Database query
Use:
Stream<T>
when you need a sequence of events over time, such as:
Chat messages
Firestore snapshots
Authentication changes
Location updates
WebSocket events
Sensor data
In Flutter UI, the same distinction usually becomes:
Future<T>
↓
FutureBuilder<T>
Stream<T>
↓
StreamBuilder<T>
Most importantly, do not choose a Stream simply because it sounds more powerful, and do not force a Future into a continuously changing data problem.
Choose the abstraction that matches the lifecycle of your data:
One eventual result → Future. Multiple asynchronous events over time → Stream.
Once this distinction is clear, Dart’s asynchronous APIs—and many Flutter libraries built on top of them—become much easier to design and understand.




