Sparround

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 is null

These beat building a list with imperative if/for blocks because they are shorter and can appear inside a const.

ConstructionResultWhat is allowed
`[1, 2, 3]`Growable list`add`, `remove`, `length = n`
`List.filled(3, 0)`Fixed-length listElements can be changed, `add` throws `UnsupportedError`
`List.unmodifiable([1, 2])`Read-only viewNo mutation at all
`const [1, 2]`Canonicalised immutable listNo mutation at all
`someIterable.toList()`New growable listFull 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);, calling active.length and then active.first runs isActive twice. 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.

MethodReturnsLazy?Typical use
`map``Iterable<R>`YesTransformation
`where``Iterable<T>`YesFiltering
`expand``Iterable<R>`YesflatMap — flattening nested lists
`fold`A single valueNoAggregation with a seed value
`reduce`A single valueNoAggregating a non-empty collection
`any` / `every``bool`No (short-circuits)Predicate checks
`toList` / `toSet`A collectionNoMaterialising 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