Collections and Iterable
Three core collections: List (ordered, allows duplicates), Set (unique elements, based on ==/hashCode), Map (key-value). All three default to a LinkedHash... implementation, meaning insertion order is preserved.
Make it a habit to spell out the type in literals: an empty {} is a Map, so an empty set is written <String>{}. Likewise an empty [] that cannot infer from context becomes dynamic — write <User>[].
Dart's collection construction syntax is one of its most useful features:
- collection-if:
[if (isAdmin) 'delete'] - collection-for:
[for (final u in users) u.name] - spread:
[...base, 'extra'] - null-aware spread:
[...?maybeNull]— adds nothing when the value isnull
These beat building a list with imperative if/for blocks because they are shorter and can appear inside a const.
| Construction | Result | What is allowed |
|---|---|---|
| `[1, 2, 3]` | Growable list | `add`, `remove`, `length = n` |
| `List.filled(3, 0)` | Fixed-length list | Elements can be changed, `add` throws `UnsupportedError` |
| `List.unmodifiable([1, 2])` | Read-only view | No mutation at all |
| `const [1, 2]` | Canonicalised immutable list | No mutation at all |
| `someIterable.toList()` | New growable list | Full mutation |
Iterable laziness — the most important part of this topic. map, where, expand, take, skip and followedBy compute nothing immediately. They return a lazy `Iterable`: the code runs only when iteration starts (for-in, toList(), first, length, fold).
Two consequences follow:
- Side effects happen at an unexpected time.
users.map((u) => log(u));on its own prints nothing, because nobody iterates it. - The chain re-executes on every iteration. With
final active = users.where(isActive);, callingactive.lengthand thenactive.firstrunsisActivetwice. If the source list changed in between, the two results can disagree.
The rule: if you will use the result more than once, call `toList()` (or `toSet()`). Conversely, when you only need the first match out of a large source, laziness works for you — where(...).first does not walk the whole list.
One more trap: mutating the source collection while iterating a lazy Iterable throws ConcurrentModificationError.
| Method | Returns | Lazy? | Typical use |
|---|---|---|---|
| `map` | `Iterable<R>` | Yes | Transformation |
| `where` | `Iterable<T>` | Yes | Filtering |
| `expand` | `Iterable<R>` | Yes | flatMap — flattening nested lists |
| `fold` | A single value | No | Aggregation with a seed value |
| `reduce` | A single value | No | Aggregating a non-empty collection |
| `any` / `every` | `bool` | No (short-circuits) | Predicate checks |
| `toList` / `toSet` | A collection | No | Materialising the result |
Interview tip. "What does map return?" looks easy, but the real test is laziness. A strong answer: "An `Iterable`, not a `List` — and it is lazy, so the function is not called until iteration starts. If I will read the result more than once I call `toList()`, otherwise the chain re-executes each time."
The most common mistake is treating map like JavaScript's, as something that immediately returns a new array. The second is not knowing the fold/reduce difference — reduce throws StateError on an empty collection, while fold is safe thanks to its seed value and can change the type (List<int> to String).
📚 Sources and documentation
- Collectionsofficialdart.dev
- Iterable collectionsofficialdart.dev
The lazy behaviour of Iterable is documented here.