Mapping, Filtering, and Reducing Collections
In this chapter, we look at three patterns that turn repetitive list-processing code into short, clear expressions: map (transform every item), where (keep some items, drop others), and reduce or fold (combine everything into one value). These patterns exist in almost every modern language, and in Dart they are part of the standard library, so you can use them on any List, Set, or Map keys.
Prerequisites
This chapter assumes you have read the previous chapters on collections and functions. You should know how to create a List, how to write a simple function, and what an anonymous function looks like in Dart, for example (n) => n * 2.
The core idea: functions as values
All three patterns rest on one idea: in Dart, a function can be treated as a value. You can store it in a variable, pass it to another function, and have that function call it. A function that takes another function as an argument is called a higher-order function.
Think of it like a sorting machine on a conveyor belt. The machine (say, map) is fixed, but you choose the stamping tool that goes inside it. You hand the tool over as an argument, and the machine applies it to every item that passes by.
maptakes a function that converts one item into another item.wheretakes a function that returnstrueorfalsefor each item, called a predicate.reduceandfoldtake a function that combines two values into one.
map: transforming every item
map produces a new sequence where every item has been transformed by your function. It does not change the original list.
void main() {
final prices = [1.50, 2.25, 3.00];
// A named function used as a value.
double withTax(double price) => price * 1.20;
final taxed = prices.map(withTax).toList();
print(taxed); // [1.8, 2.7, 3.6000000000000001]
}Two details matter here. First, you usually call .toList() on the result, because map returns a lazy Iterable — a sequence that computes items only when you ask for them. That laziness is efficient when you chain steps, but you need a concrete list if you want indexing or .length. Second, notice the odd number at the end: this is floating-point rounding, not a Dart bug. Rounding money in real code is a separate topic.
Most of the time you will write the transformation inline as an anonymous function:
void main() {
final names = ['ada', 'alan', 'grace'];
final upper = names.map((n) => n.toUpperCase()).toList();
print(upper); // [ADA, ALAN, GRACE]
}where: keeping only what matches
where filters a sequence, keeping items for which your predicate returns true.
void main() {
final words = ['apple', 'fig', 'banana', 'kiwi'];
final longWords = words.where((w) => w.length >= 4).toList();
print(longWords); // [apple, banana]
}where never changes items; it only decides which ones survive. You can chain it with map freely, because both work on any Iterable.
Combining steps: build a pipeline
The real power appears when you chain the steps. Because each step returns an Iterable, you can keep calling methods on the result:
void main() {
final names = ['Ada', 'Alan', 'Grace', 'Edsger'];
final result = names
.where((n) => n.length > 3)
.map((n) => n.toUpperCase())
.toList();
print(result); // [GRACE, EDSGER]
}Because map and where are lazy, Dart walks the items through the whole pipeline one item at a time, rather than building a full intermediate list after each step. For large collections, that saves memory and time.
reduce and fold: combining into one value
Sometimes you want a single result: a total, a maximum, a joined string. reduce takes the first item as the starting value, then repeatedly applies your combining function to the running result and the next item.
void main() {
final scores = [3, 5, 2, 8];
final total = scores.reduce((a, b) => a + b);
print(total); // 18
}Behind the scenes this computes ((3 + 5) + 2) + 8. The function always receives two arguments: the accumulated value and the next item.
reduce has one catch: on an empty collection it throws an error, because there is no starting value. fold solves this by letting you supply an initial value, and it can also change the result type:
void main() {
final scores = [3, 5, 2, 8];
final sum = scores.fold(0, (a, b) => a + b); // int
final labelled = scores.fold<int>(0, (a, b) => a + b);
final empty = <int>[];
print(sum); // 18
print(labelled); // 18
print(empty.fold(0, (a, b) => a + b)); // 0, no crash
}A common style rule: prefer fold with an explicit initial value when the collection might be empty, and prefer reduce when you are sure there is at least one item and want to start from it.
Dart also provides shortcuts for the most common folds: sum via Iterable.sum (with package collection), any, every, contains, and join:
void main() {
final scores = [3, 5, 2, 8];
print(scores.any((s) => s > 7)); // true
print(scores.every((s) => s > 0)); // true
print(['a', 'b'].join('-')); // a-b
}Other members of the family
Once you know these three, the rest of the Iterable toolbox follows the same pattern:
firstWhere: find the first item matching a predicate, or throw if none matches.take(n)andskip(n): take the first n items, or drop them.expand: map each item to zero or more items (flatten nested lists).forEach: likemap, but for side effects such as printing, with no result collected.
void main() {
final nested = [[1, 2], [3, 4]];
print(nested.expand((pair) => pair).toList()); // [1, 2, 3, 4]
}Common mistakes to avoid
- Forgetting
.toList()and then trying to index the result.Iterablehas no[]operator; call.toList()first. - Calling
reduceon a possibly empty list. Usefoldwith an initial value instead. - Reusing the same variable name for the accumulator and the item in
reduce, which makes the logic hard to read. Choose clear names like(total, item). - Using
mapfor side effects like printing. That is whatforEachis for.
Exercise
You have a list of order amounts [12.0, 7.5, 30.0]. Write a chain that keeps only amounts above 10, then computes their total. Why is fold a safer choice than reduce here?
Show answer
final total = orders.where((o) => o > 10).fold(0.0, (a, b) => a + b); — fold is safer because you supply an initial value, so the code works even if the filtered list is empty (it returns 0.0), while reduce would throw on an empty list.
Exercise
What is the difference between .map(f).toList() and calling forEach on a list?
Show answer
map builds and returns a new sequence of transformed values (and needs .toList() to become a concrete list), while forEach performs an action for each item and returns nothing. Use map when you want the transformed results; use forEach for side effects like printing.
Spotted a mistake, or code that does not work?