Collections and functional iteration
Use List, Set and Map well, spread and collection-if, and transform data with where, map and fold instead of manual loops.
List, Set and Map literals
void main() {
// growable by default; const makes it immutable and canonicalised
final names = <String>['ada', 'grace', 'alan'];
final fixed = List<int>.filled(3, 0); // length fixed at 3
final frozen = const [1, 2, 3]; // compile-time constant
// a Set removes duplicates and answers contains in constant time
final tags = <String>{'dart', 'flutter', 'dart'}; // size 2
final ordered = <String>{'b', 'a'}; // LinkedHashSet: keeps insertion order
// Map: keys are unique, lookup is by hashCode and ==
final ages = <String, int>{'ada': 36, 'grace': 45};
ages['alan'] = 41;
final updated = {...ages, 'ada': 37}; // a new map, original untouched
// spread and collection-if / collection-for inside a literal
final more = [...names, 'linus'];
final maybe = [1, if (names.length > 2) 2, for (final n in names) n.length];
final merged = {...ages, 'ada': 100}; // later key wins
// null-aware spread: skipped when the source is null
List<String>? extra;
final safe = [...names, ...?extra];
print(more);
print(maybe);
print(merged);
print(tags.length);
print(frozen);
}| Collection | Ordered | Duplicates | Lookup |
|---|---|---|---|
List | Yes, by index | Allowed | O(n) by value, O(1) by index |
Set | Insertion order by default | Rejected | O(1) average |
Map | Insertion order by default | Keys unique | O(1) average |
SplayTreeSet | Sorted | Rejected | O(log n) |
Queue | FIFO | Allowed | O(1) add and remove |
LinkedList | Insertion order | Allowed | O(1) at an entry |
A const collection is deeply immutable and canonicalised: two identical const literals in different files are the same object. That makes them cheap to compare with identical and safe to share, but any attempt to modify one throws at run time.
where, map, fold and reduce
void main() {
final numbers = [1, 2, 3, 4, 5, 6];
// lazy: where and map return Iterables that compute on demand
final evens = numbers.where((n) => n.isEven);
final squares = numbers.map((n) => n * n);
final chained = numbers.where((n) => n.isOdd).map((n) => n * 10);
// eager: toList, toSet, fold, reduce
final list = chained.toList();
final sum = numbers.fold<int>(0, (acc, n) => acc + n);
final product = numbers.reduce((a, b) => a * b); // no seed; throws on empty
// expand flattens a nested result
final letters = ['ab', 'cd'].expand((s) => s.split('')).toList();
// group and count
final byParity = <String, List<int>>{};
for (final n in numbers) {
byParity.putIfAbsent(n.isEven ? 'even' : 'odd', () => []).add(n);
}
// any, every, contains, firstWhere with orElse
final hasBig = numbers.any((n) => n > 5);
final allPositive = numbers.every((n) => n > 0);
final firstEven = numbers.firstWhere((n) => n.isEven, orElse: () => -1);
final single = [7].single; // throws unless exactly one
print([evens.toList(), squares.toList(), list, sum, product]);
print([letters, byParity, hasBig, allPositive, firstEven, single]);
}whereandmapare lazy and rebuild the computation on every iteration. Assign to atoList()result if you iterate more than once, or the filter runs again each time.reducehas no initial value and throws aStateErroron an empty iterable.foldtakes a seed and is safe for an empty collection.firstWherethrowsStateErrorwhen nothing matches unless you passorElse.firstOrNullfrompackage:collectionis the cleaner form.- A
forEachcallback cannot usebreakorcontinue. Use a plainforloop when you need to stop early. for (final x in iterable)is the idiomatic loop and calls the iterator once; indexing withfor (var i = 0; ...)is only worth it when you need the index.
// sorting: List.sort mutates and returns void
final words = ['pear', 'apple', 'fig'];
words.sort(); // natural order
words.sort((a, b) => b.length.compareTo(a.length)); // longest first
final sortedCopy = [...words]..sort(); // sort a copy, keep the original
final byLength = SplayTreeSet<String>((a, b) => a.length.compareTo(b.length));
// null-aware access keeps collection code short
final map = <String, int>{'a': 1};
final missing = map['b'] ?? 0;
final nested = <String, List<int>>{};
nested['a']?.add(1); // no error when the key is absent
(nested['a'] ??= []).add(2); // create the list on first use
// cascade: build and configure without repeating the variable
final buffer = StringBuffer()
..write('id=')
..write(42);
print(buffer.toString());Equality and hashing
class Point {
const Point(this.x, this.y);
final int x;
final int y;
// == and hashCode must be overridden together
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Point && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(x, y);
@override
String toString() => 'Point($x, $y)';
}
void main() {
final a = Point(1, 2);
final b = Point(1, 2);
print(a == b); // true only because == is overridden
print({a, b}.length); // 1: the set uses == and hashCode
// List, Set and Map use identity equality by default
print([1, 2] == [1, 2]); // false
print(const [1, 2] == const [1, 2]); // true: const canonicalisation
// use package:collection for structural comparison
// import 'package:collection/collection.dart';
// print(const ListEquality().equals([1, 2], [1, 2])); // true
// print(const DeepCollectionEquality().equals(nested1, nested2));
}💡
If you override
== without hashCode, a value placed in a Set or used as a Map key becomes unreachable: the lookup computes a different hash and never finds the bucket. Always override both, and derive the hash from the same fields the equality uses.FAQ
List or Set or Map?
A
List when order and duplicates matter and you index by position. A Set when you need membership tests or uniqueness. A Map when you look up by a key. Converting a list to a set for a lookup inside a loop turns O(n squared) into O(n).Why is my map lookup not finding an equal key?
The key type does not override
hashCode, so two logically equal objects hash differently. Override both == and hashCode, or use a value type such as a record, which has structural equality built in.Related
Dart syntax and null safety Generics, extensions and operator overloading
Last refreshed 2026-09-18.