skip to content

Built-In Containers

List, Set and Map are Dart's everyday containers: growable or fixed-length lists, and hash-based sets and maps that keep insertion order. Interviewers probe key equality and Map update idioms.

part ofDartoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Dart, what is the difference between a growable and a fixed-length List, and which does List.filled create by default?

level: juniorimportance: must knowfreq 52%

answer

  1. same static type, different runtime behaviour
  2. literals and generate grow
  3. filled and empty default growable: false
  4. length changes throw UnsupportedError
  5. one shared fill object

basics

~20 s

A growable Dart List can add and remove elements; a fixed-length one only lets you overwrite them, and length changes throw UnsupportedError. List.filled and List.empty default to fixed-length, while literals, List.of and List.generate are growable.

solid answer

~40 s

Both are plain `List<E>`; the difference is runtime behaviour. A **growable** list — from a literal, `List.of`, `List.from`, `List.generate` or `toList()` — supports `add`, `remove`, `insert` and `clear`. A **fixed-length** list — the default for `List.filled` and `List.empty` — lets you overwrite elements, `sort` or `setRange`, but anything that changes the length throws an `UnsupportedError` at runtime. Each constructor has a `growable:` argument to override the default. Two traps: `List.filled(3, [])` stores the **same** inner list in every slot, so use `List.generate` for fresh objects; and with null safety the unnamed `List()` constructor is gone and you cannot grow a non-nullable list by setting `length`.

code

dart · 17 lines
dart
void main() {
  final fixed = List<int>.filled(3, 0);
  fixed[1] = 42; // fine: overwrite
  try {
    fixed.add(7);
  } on UnsupportedError catch (e) {
    print(e); // Unsupported operation: Cannot add to a fixed-length list
  }

  final growable = List<int>.filled(3, 0, growable: true);
  growable.add(7);
  print(growable); // [0, 0, 0, 7]

  final rows = List.generate(2, (_) => <int>[]);
  rows[0].add(1);
  print(rows); // [[1], []]
}

go deeper

for a junior

Know which constructors give growable lists and which give fixed-length ones, and that List.filled shares one fill object across all slots.

for a middle

Explain which operations a fixed-length list allows, why the failure is a runtime UnsupportedError, and what null safety removed.

for a senior

Use fixed-length lists deliberately where size is an invariant, and catch shared-fill and List.empty default bugs in review.

for a principal

Set conventions for how lists are created in shared code so that fixed versus growable intent is visible to readers and reviewers.

## Two kinds of List Every Dart `List` is either **growable** or **fixed-length**, and which one you get depends on how it was created, not on its type: both are plain `List<E>`, and nothing in the static type tells them apart. - A **growable list** supports the whole `List` API. It keeps an internal buffer that grows when needed, so a sequence of `add` calls runs in amortized constant time. - A **fixed-length list** has a length set at creation. You can read and overwrite its elements, but any operation that would change the length throws an `UnsupportedError` at runtime (for example *Cannot add to a fixed-length list*). ## Which constructor gives which | How the list is created | Default | |---|---| | a literal: `[]`, `<int>[1, 2]` | growable | | `List.of(iterable)` / `List.from(iterable)` | growable (`growable: true`) | | `List.generate(n, (i) => ...)` | growable (`growable: true`) | | `iterable.toList()` | growable (`growable: true`) | | `List.filled(n, value)` | **fixed-length** (`growable: false`) | | `List.empty()` | **fixed-length**, length 0 (`growable: false`) | Every constructor in the table except the literal accepts a `growable:` named argument to flip the default. `List.empty()` with its default is a fixed-length list of length zero, which is almost never what someone reaching for "an empty list" wants; `<E>[]` or `List.empty(growable: true)` is. ## What a fixed-length list allows Allowed, because the length does not change: - index assignment `list[i] = v`, `setAll`, `setRange`, `fillRange`; - `sort` and `shuffle`, which reorder in place. Throws `UnsupportedError`, because the length would change: - `add`, `addAll`, `insert`, `insertAll`; - `remove`, `removeAt`, `removeLast`, `removeWhere`, `clear`; - assigning to `length`. Choose a fixed-length list when the size is part of the meaning — a buffer of 26 letter counters, a grid of cells — so that an accidental `add` fails loudly instead of silently corrupting the shape. ## The List.filled sharing trap `List.filled(n, fill)` stores **the same `fill` object** at every position. For immutable values such as `0` or `''` that is harmless. For a mutable object it is a classic bug: ```dart final shared = List.filled(3, <String>[]); shared[0].add('dart'); print(shared); // [[dart], [dart], [dart]] final separate = List.generate(3, (_) => <String>[]); separate[0].add('dart'); print(separate); // [[dart], [], []] ``` `List.generate` calls its generator **once per index**, in increasing index order, so each slot gets its own object. It also gives you the index, which makes it the natural way to build computed lists such as `List.generate(5, (i) => i * i)`. ## Null safety changed two things 1. The **unnamed `List()` constructor is gone**. It created a list of a given length with uninitialized slots, which would break soundness for a non-nullable element type, so calling `List()` is an error in null-safe code. Use a literal, `List.filled`, `List.generate` or `List.empty` instead. 2. **Growing a list by setting `length`** is only possible when the element type is nullable, because new slots are filled with `null`. `<int>[1, 2, 3]..length = 5` throws at runtime; truncating (`length = 1`) is always fine on a growable list. ## Putting it together ```dart final letterCounts = List<int>.filled(26, 0); // fixed, 26 zeros letterCounts['c'.codeUnitAt(0) - 97]++; // allowed: overwrite final words = <String>[]; // growable words.add('hello'); // allowed // letterCounts.add(1); // UnsupportedError at runtime ``` ## Interview summary - **Defaults:** literals, `List.of`, `List.from`, `List.generate` and `toList()` grow; `List.filled` and `List.empty` do not, unless `growable: true` is passed. - **Failure mode:** changing the length of a fixed-length list is a **runtime** `UnsupportedError`, not a compile error, because both kinds share the `List` type. - **Allowed on fixed-length:** overwriting, `sort`, `shuffle`, `setRange`, `fillRange`. - **Shared fill:** `List.filled` puts one object in every slot; `List.generate` creates one per index. - **Null safety:** no `List()` constructor, and no growing a non-nullable list through `length`. A good answer also says *why* you would pick one: fixed-length when the size is an invariant of the data, growable everywhere else — which in everyday Flutter and Dart code means the literal almost every time.

  • Why does List.filled(3, <String>[]) behave surprisingly in Dart when you add to one element?
    `List.filled` stores the same `fill` object at every index, so all three slots point to one inner list and an `add` through any index shows up in all of them. `List.generate(3, (_) => <String>[])` calls the generator per index and gives each slot its own list.
  • What replaced the unnamed List() constructor in null-safe Dart?
    Nothing directly: `List()` made uninitialized slots, which is unsound for non-nullable element types, so it was removed. Use a literal for an empty growable list, `List.filled` or `List.generate` for a known length, and `List.empty(growable: true)` if you need a constructor call.
  • Can you sort a fixed-length Dart List?
    Yes. `sort`, `shuffle`, index assignment, `setAll`, `setRange` and `fillRange` keep the length, so they work on a fixed-length list. Only operations that add or remove elements, or assign `length`, throw `UnsupportedError`.

saying these in an interview costs you the question

  • List.filled returns a growable list by default.
  • Adding to a fixed-length list is a compile-time error.
  • A fixed-length list cannot have its elements changed.
  • List.filled(3, []) creates three separate inner lists.
  • List() is the way to make an empty list in modern Dart.
open as a page

When counting word frequencies in a Dart Map<String, int>, how do update, putIfAbsent and the [] operator differ?

level: middleimportance: must knowfreq 50%

basics

~20 s

In a Dart map, [] returns a nullable value, so counting needs counts[w] = (counts[w] ?? 0) + 1. update(w, (n) => n + 1, ifAbsent: () => 1) does it in one call; putIfAbsent returns the stored value and suits get-or-create grouping.

open as a page

In Dart, what iteration order does a Map or Set literal guarantee, and when would you choose HashMap or SplayTreeMap instead?

level: middleimportance: should knowfreq 40%

basics

~10 s

Dart map and set literals create a LinkedHashMap or LinkedHashSet, which iterate in key insertion order. Pick HashMap when order truly does not matter, and SplayTreeMap when keys must stay sorted by a comparator.

open as a page

Why can a Dart Map or Set fail to find a key that was inserted earlier, and how do == and hashCode cause it?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Dart hash maps and sets locate keys by hashCode, then confirm with ==. A key without value equality (a List or plain class), a key mutated after insertion, or inconsistent == and hashCode makes an entry exist yet be unfindable.

open as a page