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.
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.
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*.
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.
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.
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.
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.
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.
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
- Lazy vs eager: the results of
mapandwhereare lazy; calltoListortoSetwhen you need the results more than once. - Side effects: avoid side effects in functions passed to
mapandwhere; they run each time the iterable is iterated. Use aforloop for work done for its effects. - Infinite sequences: limit them with
takeortakeWhilebefore consuming them. - Empty iterables:
reduce,first,lastandsinglethrow on an empty iterable; preferfold,firstOrNullorfirstWherewithorElsewhen the input may be empty. - Parameter types: accept
Iterable<T>instead ofList<T>when you only read the elements; callers can then pass any collection. - Length:
lengthis O(1) for a list but O(n) for a general iterable; useisEmptyrather thanlength == 0.
Source
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
List all Dart tutorials.