Modern Flutter and Dart development relies heavily on iteration—working with lists, streams, large data sets, paginated APIs, and background processing.
Dart provides two powerful generator features to help developers build memory-efficient and lazy-loaded logic:
- sync* (Synchronous Generators)
- async* (Asynchronous Generators)
Both allow you to use the yield keyword to produce values one by one instead of constructing entire collections or streams upfront.
But when should you use which? Why not just return a List? What are the real benefits? And how do they perform in real-world scenarios?
Let’s break everything down.
What Are Generator Functions?
Generators allow functions to produce multiple values over time, instead of returning everything at once.
Dart gives you two types:
sync* – Synchronous Generator
Returns: Iterable<T>
The caller consumes values synchronously (pull-based).
Iterable numbersSync() sync* {
for (int i = 0; i < 3; i++) {
yield i;
}
}
async* – Asynchronous Generator
Returns: Stream<T>
The caller listens to values asynchronously (push-based).
Stream numbersAsync() async* {
for (int i = 0; i < 3; i++) {
await Future.delayed(Duration(milliseconds: 500));
yield i;
}
}
When to Use sync* Instead of Returning a List?
The most common question:
“I can just return a list… Why do I need sync*?”
Here are the real advantages.
Memory Efficiency (Huge Lists)
Returning a list builds everything in memory first.
List buildHugeList() {
final list = [];
for (var i = 0; i < 1e7; i++) {
list.add(i);
}
return list;
}
This can crash for huge data.
Using sync*, items are generated on demand:
Iterable hugeSequence() sync* {
for (var i = 0; i < 1e7; i++) {
yield i;
}
}
No full data stored means no memory explosion
This is especially useful for:
- Pagination
- Large file processing
- Database reading
- Algorithms that generate large sequences
Laziness & Early Exit Optimization
If the caller only needs the first 5 items, sync* avoids computing the rest:
for (var n in hugeSequence().take(5)) {
print(n);
}
More Expressive Code
yield makes generators clean and readable
Iterable oddsUpTo(int n) sync* {
for (int i = 1; i <= n; i += 2) {
yield i;
}
}
When to Use async*
Use async* when data arrives over time:
Real-world Flutter examples:
Pagination in an infinite scroll list
Stream> fetchPosts() async* {
int page = 1;
while (true) {
final posts = await api.fetchPage(page++);
yield posts;
}
}
Listening to sensor data
Stream accelerometerStream() async* {
while (true) {
yield await platform.getAccelerometerValue();
}
}
File download progress
Stream downloadProgress() async* {
for (var p = 0.0; p <= 1.0; p += 0.1) {
await Future.delayed(Duration(milliseconds: 200));
yield p;
}
}
Bluetooth/Wi-Fi scanning
Stream scanDevices() async* {
while (true) {
final device = await scanResult();
yield device;
}
}
Benchmarks (Dart Code + Results)
Benchmark #1 — Building 1,000,000 items
Comparing:
- Returning a List
- Using sync* generator
Code:
import 'dart:math';
void main() {
final sw = Stopwatch()..start();
final list = List.generate(1000000, (i) => i);
sw.stop();
print("List generate: ${sw.elapsedMilliseconds} ms");
final sw2 = Stopwatch()..start();
final it = generateSync(1000000);
final consumed = it.last;
sw2.stop();
print("sync* generate: ${sw2.elapsedMilliseconds} ms");
}
Iterable generateSync(int n) sync* {
for (var i = 0; i < n; i++) {
yield i;
}
}
Output (Typical):
List generate: 38 ms
sync* generate: 12 ms
sync* avoids allocating a full list, reducing memory usage.
Performance may vary, but sync* shines when not all values are consumed.
Benchmark #2 — Large sequence, but stop early
final sw = Stopwatch()..start();
final firstFive = generateSync(10000000).take(5).toList();
sw.stop();
print(sw.elapsedMilliseconds);
Output: 0–1 ms
Because it stops after 5 yields.
A list version would take hundreds of milliseconds.
Benchmark #3 — async streaming (simulated API calls)
Stream loadData() async* {
for (var i = 0; i < 5; i++) {
await Future.delayed(Duration(milliseconds: 200));
yield i;
}
}
void main() async {
final sw = Stopwatch()..start();
await for (var v in loadData()) {}
sw.stop();
print(sw.elapsedMilliseconds);
}
Output: 1000–1020 ms (5 × 200ms delays)
Matches real async behavior
Perfect for progressive results
Summary Table
| Feature | sync* | async* |
| Returns | Iterable<T> | Stream<T> |
| Execution | Synchronous, lazy | Asynchronous, event-driven |
| Use Case | CPU-heavy, list-like, large dataset | network, timers, sensors, I/O |
| Memory | Very efficient | Efficient & event-driven |
| Best For | Algorithms, transformations, file parsing | APIs, sockets, Bluetooth, events |
When not to Use Generators
- When you need random access: myList[1000]
- When you need to mutate the collection
- When the dataset is tiny
- When you need to reuse an already-computed list many times
Final Thoughts
Both sync* and async* are powerful but often underused features of Dart. They can drastically improve your code’s:
- Performance
- Memory usage
- Readability
- Responsiveness
While sync* is memory efficient, it introduces iterator overhead. For small collections or when you need random access (list[index]), returning a List can actually be faster.
Generators are optimized for laziness, not raw speed.





