In Dart, what is the difference between an Iterable and a List, and why does list.map(...) not give you a List?
answer
- anything you can step through
- no [] operator on Iterable
- map and where return lazy views
- nothing runs until iterated
- toList materialises and snapshots
basics
~20 sA Dart Iterable is any sequence you can step through; a List adds indexing, a stored length and mutation. map, where and similar methods return a lazy Iterable that computes nothing until iterated, so you call toList() to get a List.
solid answer
~40 s`Iterable<E>` only promises an `iterator`; `List<E>` is an `Iterable` that also offers `list[i]`, a constant-time `length` and, if growable, mutation. On a general `Iterable` there is no `[]` — `elementAt` may step from the start — and `length` may count by iterating. Methods such as `map`, `where`, `expand`, `take`, `skip` and `whereType` return a **new lazy `Iterable`**: calling them does no work, and the callbacks run only when something iterates the result, such as a `for` loop, `toList()`, `first` or `fold`. So `List<String> x = lines.map(f);` is a type error; add `.toList()` to materialise the values, which also lets you index, read a cheap length, iterate repeatedly without recomputation and keep a snapshot independent of the source.
code
dart · 14 linesvoid main() {
final lines = ['INFO start', 'ERROR disk full', 'INFO done'];
final tags = lines.map((l) => l.split(' ').first);
print(tags); // (INFO, ERROR, INFO) - an Iterable prints in parentheses
final tagList = tags.toList();
print(tagList[1]); // ERROR
print(tagList); // [INFO, ERROR, INFO]
for (final (i, line) in lines.indexed) {
print('$i: $line');
}
}go deeper
Know that map and where return an Iterable, not a List, and that toList() converts it when you need indexing or a List-typed API.
Explain laziness: nothing runs until iteration, length and elementAt may walk the sequence, and toList() snapshots the result.
Decide where a pipeline should be materialised and where it should stay lazy, based on how many times and how far it is consumed.
Guide API design so functions return Iterable only when laziness is intended and List when callers need stored, stable results.
## Two levels of the collection hierarchy In Dart, **`Iterable<E>`** is the general interface for "a sequence of values you can step through". Its only required member is the `iterator` getter; everything else — `map`, `where`, `contains`, `length`, `first`, `fold` — is defined in terms of stepping through that iterator. `List`, `Set`, the `keys` and `values` of a `Map`, the result of a `sync*` generator and the result of `map` or `where` are all `Iterable`s. **`List<E>`** is a specific `Iterable` that also promises **indexed access** and a known length: | | `Iterable<E>` | `List<E>` | |---|---|---| | Element access | `elementAt(i)`, possibly by stepping from the start | `list[i]`, direct | | `length` | may iterate every element to count them | stored, constant time | | Mutation | none in the interface | `add`, `remove`, `[]=` (if growable) | | Evaluation | may be computed lazily, on every pass | elements are stored | | `toString()` | `(a, b, c)` in parentheses | `[a, b, c]` in brackets | Because an `Iterable` cannot promise cheap random access, it has no `[]` operator; you use `elementAt`, which the Dart docs warn can be linear. ## Why map returns an Iterable `Iterable.map`, `where`, `expand`, `take`, `skip`, `takeWhile`, `skipWhile` and `whereType` all return a **new, lazy `Iterable`**. Nothing is computed when you call them. The work happens only when something iterates the result: a `for-in` loop, `toList()`, `toSet()`, `first`, `fold`, `join`, `contains` and so on. That is why this does not compile: ```dart final lines = ['INFO start', 'ERROR disk full', 'INFO done']; List<String> upper = lines.map((l) => l.toUpperCase()); // error ``` `lines.map(...)` has static type `Iterable<String>`, which is not assignable to `List<String>`. Add `.toList()` to **materialise** the result into a list. `toSet()` does the same into a `LinkedHashSet`, dropping duplicates. ## When to convert with toList Call `toList()` (or `toSet()`) when you need: - an API that demands a `List`, such as a Flutter `Column`'s `children`; - indexing (`result[3]`) or a cheap `length`; - to iterate the result **more than once** without recomputing it; - a **snapshot** that no longer reflects later changes to the source collection; - to mutate the result. Keep the `Iterable` when you only pass it on to one consumer that iterates once — `join`, a single `for` loop, `fold` — or when you might stop early with `first`, `any` or `take`, because then the lazy form avoids work on the elements you never reach. ## A few Iterable-only helpers - `whereType<T>()` filters **and** narrows the static type, so `mixed.whereType<int>()` is an `Iterable<int>`; `where((e) => e is int)` would still be `Iterable<Object>`. - `indexed` (Dart 3.0) yields `(int, E)` records: `for (final (i, line) in lines.indexed) ...`. - `firstOrNull`, `lastOrNull`, `singleOrNull` and `elementAtOrNull` (Dart 3.0) return `null` instead of throwing on a missing element. They are defined in `dart:collection` and exported from `dart:core`, so no import is needed. ## Common mistakes 1. **Assigning `map(...)` to a `List`** — a compile error, fixed with `.toList()`. 2. **Calling `.toList()` by reflex** — harmless for small inputs, but it allocates a list you may never need, for example before a single `join`. 3. **Using `elementAt` in a loop on a lazy `Iterable`** — each call may step from the beginning, making the loop quadratic; convert once with `toList()`. 4. **Expecting `map` to run its callback immediately** — a `map` whose result is never iterated never calls its function at all. ## Interview summary - `Iterable` = anything you can iterate; `List` = an `Iterable` with indexes, a stored length and mutation. - `map`, `where` and friends return **lazy** `Iterable`s; nothing runs until iteration. - `toList()` / `toSet()` materialise, snapshot and allow indexing.
- Why is calling elementAt(i) inside a loop over a lazy Dart Iterable a performance trap?A general `Iterable` has no random access, so `elementAt(i)` may step through `i` elements from the start, and on a lazy chain it reruns the callbacks too. Looping over indexes then costs quadratic time. Iterate directly with `for-in`, or call `toList()` once and index the list.
- What does whereType<int>() give you in Dart that where((e) => e is int) does not?The right static type. `whereType<int>()` returns `Iterable<int>`, while the `where` version keeps the original element type, such as `Iterable<Object>`, and would need a `cast<int>()` wrapper. Effective Dart recommends `whereType` for filtering by type.
- Does a Dart map() callback run if nobody iterates its result?No. `map` only builds a lazy wrapper; the function runs when an element is requested. A `map` written purely for its side effects, with the result discarded, does nothing. Use a `for` loop for side effects.
saying these in an interview costs you the question
- list.map(...) returns a new List.
- map runs its callback as soon as it is called.
- Every Iterable supports iterable[i] indexing.
- Iterable.length is always constant time.
- toList() is required before a for-in loop.