ZetCode

Dart Iterable

last modified October 18, 2026

In this article, we show how to work with Iterable in Dart. An Iterable is a collection of elements that can be accessed sequentially. It is defined in dart:core.

Iterable does not support indexed access and has no add or remove methods. Its job is to provide elements one after another and to offer a rich set of methods for processing them. The core collections List, Set and Queue, as well as the keys and values of a Map, all implement Iterable.

Most of the methods, such as map, where, take or expand, return another iterable which is lazy. It does not compute its elements until somebody iterates over it. Methods such as toList, reduce, fold or join consume the elements and produce a final result.

Basic Iterable operations

The most common operations are mapping, filtering and reducing.

main.dart
void main() {
  var numbers = [1, 2, 3, 4, 5];

  var squares = numbers.map((n) => n * n);
  print('Squares: $squares');

  var evens = numbers.where((n) => n.isEven);
  print('Evens: $evens');

  var sum = numbers.reduce((a, b) => a + b);
  print('Sum: $sum');
}

The map method transforms each element, where keeps the elements that satisfy a predicate, and reduce combines all elements into a single value. The lazy results are printed in parentheses, which is how an iterable (as opposed to a list, which uses brackets) is displayed. Note that reduce throws a StateError for an empty iterable; fold, which takes an initial value, does not.

$ dart main.dart
Squares: (1, 4, 9, 16, 25)
Evens: (2, 4)
Sum: 15

Chaining Iterable operations

The methods can be chained into a processing pipeline. Each step returns a new iterable.

main.dart
void main() {
  var words = ['apple', 'banana', 'cherry', 'date', 'elderberry'];

  var result = words
      .where((word) => word.length > 4)
      .map((word) => word.toUpperCase())
      .take(3);

  print(result);
  print(result.toList());
}

We keep the words longer than four characters, convert them to upper case and take the first three. The pipeline is evaluated when we print it or call toList. Because take is the last step, the remaining words are never processed. The word date was dropped by the where call and elderberry was left out by take.

$ dart main.dart
(APPLE, BANANA, CHERRY)
[APPLE, BANANA, CHERRY]

Generators with sync*

The simplest way to create a custom iterable is a generator function marked with sync*.

main.dart
Iterable<int> countDown(int from) sync* {
  for (var i = from; i >= 0; i--) {
    yield i;
  }
}

Iterable<int> naturals() sync* {
  var n = 1;
  while (true) {
    yield n++;
  }
}

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

  var firstSquares = naturals().map((n) => n * n).take(5);
  print(firstSquares);
}

The yield keyword produces the next element. The countDown function generates a finite sequence. The naturals function is an infinite sequence: it would never end if we converted it to a list. Since iterables are lazy, we can safely combine it with map and take, which stops after five elements.

$ dart main.dart
(5, 4, 3, 2, 1, 0)
[5, 4, 3, 2, 1, 0]
(1, 4, 9, 16, 25)

Implementing Iterable

For a collection of our own we extend IterableBase from dart:collection and provide an iterator getter. All the other methods come for free.

main.dart
import 'dart:collection';

class Range extends IterableBase<int> {
  final int start;
  final int end;

  Range(this.start, this.end);

  @override
  Iterator<int> get iterator => _RangeIterator(start, end);
}

class _RangeIterator implements Iterator<int> {
  final int _end;
  int _current;

  _RangeIterator(int start, this._end) : _current = start - 1;

  @override
  int get current => _current;

  @override
  bool moveNext() {
    if (_current >= _end) return false;
    _current++;
    return _current <= _end;
  }
}

void main() {
  var range = Range(1, 6);

  for (var n in range) {
    print(n);
  }

  print(range.where((n) => n.isOdd).toList());
  print(range.length);
  print(range.contains(4));
}

The Range class represents the integers from start to end, both inclusive. The iterator implements two members: moveNext advances to the next element and returns false when there are no more; current returns the element the iterator points to. The iterator starts one step before the first element, because moveNext must be called before the first access to current.

Our class can be used in a for loop and with methods such as where, length and contains, none of which we had to write. The inherited length counts the elements by iterating over them, so it is an O(n) operation.

$ dart main.dart
1
2
3
4
5
6
[1, 3, 5]
6
true

More Iterable methods

The expand, any, every and fold methods cover many common tasks.

main.dart
void main() {
  var matrix = [
    [1, 2],
    [3, 4],
    [5, 6]
  ];

  var flattened = matrix.expand((row) => row);
  print('Flattened: $flattened');

  print('Any negative: ${flattened.any((n) => n < 0)}');
  print('All positive: ${flattened.every((n) => n > 0)}');

  var product = flattened.fold(1, (p, n) => p * n);
  print('Product: $product');

  var text = flattened.fold('', (s, n) => '$s$n;');
  print('Text: $text');

  print('First > 3: ${flattened.firstWhere((n) => n > 3)}');
  print('Joined: ${flattened.join('-')}');
}

The expand method maps each element to an iterable and flattens the results, which turns the matrix into a flat sequence. The any method checks whether at least one element satisfies a condition and every checks all of them. The fold method is a more flexible version of reduce: it takes an initial value and the result type can differ from the element type, as in the second example where it builds a string from numbers. The firstWhere method returns the first element matching a predicate and throws a StateError if there is none, unless we pass the orElse callback. The join method concatenates the elements into a string with a separator.

$ dart main.dart
Flattened: (1, 2, 3, 4, 5, 6)
Any negative: false
All positive: true
Product: 720
Text: 1;2;3;4;5;6;
First > 3: 4
Joined: 1-2-3-4-5-6

Lazy evaluation

The following example prints a message at each step so that we can see when the work is done.

main.dart
Iterable<int> generate() sync* {
  print('Generator started');
  for (var i = 1; i <= 5; i++) {
    print('Yielding $i');
    yield i;
  }
}

void main() {
  var numbers = generate().map((n) {
    print('Mapping $n');
    return n * 2;
  }).take(2);

  print('Created pipeline');
  print('First: ${numbers.first}');
  print('---');
  print('List: ${numbers.toList()}');
}

Nothing happens when the pipeline is created; the first output line is the message printed by main. The call to first starts the generator, takes one element through map and stops; elements 2 to 5 are never generated. The call to toList then needs two elements. Notice that the generator starts again from the beginning. The iterable is not a stored sequence; every time we iterate over it, the whole pipeline is evaluated anew.

$ dart main.dart
Created pipeline
Generator started
Yielding 1
Mapping 1
First: 2
---
Generator started
Yielding 1
Mapping 1
Yielding 2
Mapping 2
List: [2, 4]

Re-evaluation and caching

The same applies to the iterables returned by map and where over a list. This matters when the function passed to map is expensive or has side effects.

main.dart
void main() {
  var numbers = [1, 2, 3, 4, 5];

  var lazy = numbers.map((n) {
    print('Computing $n');
    return n * n;
  });

  print('Pass one:');
  print(lazy.toList());

  print('Pass two:');
  print(lazy.toList());

  print('Caching the result:');
  var cached = lazy.toList();
  print('Using the cached list:');
  print(cached);
  print(cached);
}

Each call to toList runs the mapping function for all elements again. If we need the results more than once, we store them in a list with toList (or in a set with toSet) and use that collection from then on. A common bug is to keep the lazy result of map in a field and later wonder why the function is called over and over.

$ dart main.dart
Pass one:
Computing 1
Computing 2
Computing 3
Computing 4
Computing 5
[1, 4, 9, 16, 25]
Pass two:
Computing 1
Computing 2
Computing 3
Computing 4
Computing 5
[1, 4, 9, 16, 25]
Caching the result:
Computing 1
Computing 2
Computing 3
Computing 4
Computing 5
Using the cached list:
[1, 4, 9, 16, 25]
[1, 4, 9, 16, 25]

Generating and slicing iterables

Dart provides several more methods to create and cut iterables.

main.dart
void main() {
  var squares = Iterable<int>.generate(10, (i) => i * i);
  print(squares);

  print(squares.takeWhile((n) => n < 30));
  print(squares.skipWhile((n) => n < 30));
  print(squares.skip(2).take(3));

  var mixed = [1, 'two', 3, 'four', null];
  print(mixed.whereType<int>());
  print(mixed.whereType<String>());
  print(mixed.nonNulls);

  print([1, 2].followedBy([3, 4]));

  var fruits = ['apple', 'banana', 'cherry'];
  for (var (i, fruit) in fruits.indexed) {
    print('$i: $fruit');
  }
}

The Iterable.generate constructor creates an iterable of a given length from a function of the index. The takeWhile method yields elements until the condition fails, skipWhile discards elements until the condition fails, and skip and take cut by count. The whereType method filters by type and also changes the static type of the result, which makes it handy for heterogeneous lists. The nonNulls getter drops null values and returns a non-nullable iterable. The followedBy method concatenates two iterables. Finally, indexed pairs each element with its index as a record, which we destructure in the for loop.

$ dart main.dart
(0, 1, 4, 9, 16, 25, 36, 49, 64, 81)
(0, 1, 4, 9, 16, 25)
(36, 49, 64, 81)
(4, 9, 16)
(1, 3)
(two, four)
(1, two, 3, four)
(1, 2, 3, 4)
0: apple
1: banana
2: cherry

Best practices

Source

Dart Iterable documentation

We have covered the Iterable interface: transforming, filtering and reducing elements, writing generators and custom iterables, lazy evaluation and its consequences, and the methods that create and slice iterables.

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.