In Dart, what is the difference between a growable and a fixed-length List, and which does List.filled create by default?
answer
- same static type, different runtime behaviour
- literals and generate grow
- filled and empty default growable: false
- length changes throw UnsupportedError
- one shared fill object
basics
~20 sA 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 sBoth 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 linesvoid 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
Know which constructors give growable lists and which give fixed-length ones, and that List.filled shares one fill object across all slots.
Explain which operations a fixed-length list allows, why the failure is a runtime UnsupportedError, and what null safety removed.
Use fixed-length lists deliberately where size is an invariant, and catch shared-fill and List.empty default bugs in review.
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.