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);
}
CollectionOrderedDuplicatesLookup
ListYes, by indexAllowedO(n) by value, O(1) by index
SetInsertion order by defaultRejectedO(1) average
MapInsertion order by defaultKeys uniqueO(1) average
SplayTreeSetSortedRejectedO(log n)
QueueFIFOAllowedO(1) add and remove
LinkedListInsertion orderAllowedO(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]);
}
  • where and map are lazy and rebuild the computation on every iteration. Assign to a toList() result if you iterate more than once, or the filter runs again each time.
  • reduce has no initial value and throws a StateError on an empty iterable. fold takes a seed and is safe for an empty collection.
  • firstWhere throws StateError when nothing matches unless you pass orElse. firstOrNull from package:collection is the cleaner form.
  • A forEach callback cannot use break or continue. Use a plain for loop when you need to stop early.
  • for (final x in iterable) is the idiomatic loop and calls the iterator once; indexing with for (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.

Dart syntax and null safety Generics, extensions and operator overloading

Last refreshed 2026-09-18.