ZetCode

Generators in Dart

last modified October 18, 2026

In this article we explore generators in Dart, the functions that produce a sequence of values on demand using sync*, async*, and yield.

Generators Overview

A generator is a function that produces a series of values instead of returning a single one. Instead of building a whole list up front, it hands out values one at a time, lazily, as a consumer asks for them. This is efficient for large or unbounded sequences, because only the values actually used are computed.

Dart has two kinds of generators. A synchronous generator, marked with sync*, returns an Iterable and uses yield to emit values. An asynchronous generator, marked with async*, returns a Stream and may await between yields. The yield* form delegates to another generator, forwarding its values.

Form Returns Produces values with
sync* Iterable yield
async* Stream yield (may await)
yield* Delegates to another generator Forwards Iterable or Stream

Synchronous Generators

Adding sync* to a function body and using yield turns it into a generator of an Iterable. The values are produced each time the iteration advances.

sync_generator.dart
Iterable<int> countTo(int n) sync* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

void main() {
  print(countTo(5).toList());

  for (final n in countTo(3)) {
    print('n=$n');
  }
}

countTo looks like a loop, but each yield hands a value to the caller and pauses there. The first call materialises five values with toList; the loop then consumes a fresh sequence of three. The generator can be iterated as many times as it is called, because each call creates a new iteration.

$ dart run sync_generator.dart
[1, 2, 3, 4, 5]
n=1
n=2
n=3

Lazy Evaluation

Generators are lazy: no work happens until a value is requested. This makes it possible to define an infinite sequence safely, as long as the consumer only takes a finite number of values.

lazy_generator.dart
Iterable<int> naturals() sync* {
  var i = 0;
  while (true) {
    print('generating $i');
    yield i++;
  }
}

void main() {
  print('created');
  final firstThree = naturals().take(3).toList();
  print(firstThree);
}

naturals loops forever, yet the program terminates. Calling the generator prints only created; nothing is generated until take(3) pulls values. Exactly three values are produced, matching the three generating lines, and the infinite loop is abandoned once the consumer stops asking.

$ dart run lazy_generator.dart
created
generating 0
generating 1
generating 2
[0, 1, 2]

Recursive Generators and yield*

The yield* operator delegates to another generator, producing all of its values in place. This composes generators naturally and makes recursive traversal concise.

recursive_generator.dart
class Node {
  final String name;
  final List<Node> children;
  Node(this.name, [this.children = const []]);
}

Iterable<String> walk(Node node) sync* {
  yield node.name;
  for (final child in node.children) {
    yield* walk(child);
  }
}

void main() {
  final tree = Node('root', [
    Node('a', [Node('a1'), Node('a2')]),
    Node('b', [Node('b1')]),
  ]);

  print(walk(tree).toList());
}

walk yields its own name, then delegates to itself for each child with yield*. The result is a depth-first listing of the whole tree: root, then the a subtree and its children, then the b subtree. Without yield* the nested sequences would have to be flattened by hand.

$ dart run recursive_generator.dart
[root, a, a1, a2, b, b1]

Asynchronous Generators

Marking a function async* makes it produce a Stream. Inside the body you may await between yields, so each value can depend on asynchronous work such as a network request or a timer.

async_generator.dart
Stream<int> ticks(int n) async* {
  for (var i = 1; i <= n; i++) {
    await Future.delayed(const Duration(milliseconds: 20));
    yield i;
  }
}

Future<void> main() async {
  await for (final t in ticks(3)) {
    print('tick $t');
  }
  print('done');
}

Each iteration waits 20 milliseconds and then yields the next tick, so the stream delivers values over time rather than all at once. The await for loop in main receives them as they arrive and finishes when the stream is exhausted.

$ dart run async_generator.dart
tick 1
tick 2
tick 3
done

Delegating with yield* in Async Generators

yield* works in async generators too, forwarding every event from another stream. It is useful for concatenating or composing streams.

async_delegate.dart
Stream<int> numbers() async* {
  yield 1;
  yield 2;
}

Stream<int> combined() async* {
  yield* numbers();
  yield 3;
}

Future<void> main() async {
  await for (final n in combined()) {
    print(n);
  }
}

combined first delegates to numbers with yield*, emitting 1 and 2, and then yields 3 itself. The consumer sees a single stream 1, 2, 3 and never needs to know it was assembled from two generators.

$ dart run async_delegate.dart
1
2
3

Chunking a Sequence

A practical use of generators is splitting a long collection into fixed-size chunks without building all the chunks up front. The generator allocates and produces one chunk at a time as the consumer requests it.

chunking.dart
Iterable<List<int>> chunked(List<int> items, int size) sync* {
  if (size <= 0) {
    throw ArgumentError.value(size, 'size', 'must be positive');
  }

  for (var i = 0; i < items.length; i += size) {
    final end = i + size < items.length ? i + size : items.length;
    yield items.sublist(i, end);
  }
}

void main() {
  final data = List.generate(7, (i) => i + 1);
  for (final chunk in chunked(data, 3)) {
    print(chunk);
  }
}

chunked walks the list in steps of size, yielding a sublist each time and clamping the last chunk to the list length. The result is three chunks: two of size three and a final chunk of size one. Nonpositive sizes are rejected with ArgumentError when iteration begins, since a sync* body runs lazily. Because each chunk is produced on demand, the whole set of chunks never has to exist at once.

$ dart run chunking.dart
[1, 2, 3]
[4, 5, 6]
[7]

Source

Dart Language: Generators
Dart Language: Asynchronous programming
Dart Language: Iterables
Dart API: dart:async

Generators let you describe a sequence by how each value is produced rather than by building a collection. Synchronous generators with sync* return lazy Iterables, asynchronous generators with async* return Streams, and yield* composes generators, including recursively. The result is code that is both concise and memory efficient.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.