Dart 14 🎯 Core Collections: List (Fixed vs Growable) and Spread Operators (…, …?)
Lists are the most frequently used collection in Dart. A list is an ordered sequence of elements, indexed from zero, that can hold any type. But not all lists behave the same way. A fixed-length list has a size determined at creation and cannot grow or shrink; a growable list can add and remove elements freely. The distinction matters for performance, for the operations available, and for the compiler’s ability to catch mistakes.
The spread operators, introduced in Dart 2.3, brought the language a concise syntax for combining lists and other iterables. The ... operator expands an iterable into the surrounding collection, and ...? does the same but tolerates a null value instead of throwing. With spread, merging lists, prepending elements, and conditionally including elements become single expressions rather than imperative sequences. Together with collection-if and collection-for, spread makes list literals expressive enough to replace many uses of addAll, insertAll, and conditional logic.
This chapter covers list creation, the difference between fixed-length and growable lists, the operations each supports, the spread operators ... and ...?, null-aware spread, combining spread with collection-if and collection-for, and the patterns that use lists and spread idiomatically.
Key point: A fixed-length list has a size set at creation and cannot grow or shrink. A growable list can add and remove elements. The spread operator ... expands an iterable into a collection literal, and ...? does the same while tolerating null. Spread composes with collection-if and collection-for to build lists declaratively.
Why lists and spread operators exist
The ordered-collection problem. Many data structures need order: a queue of tasks, a sequence of steps, a ranking of scores. A list preserves order and allows access by index. It is the default collection for sequences.
The growable-versus-fixed problem. Some sequences are known in advance and never change: the days of the week, the fixed options of an enum, the rows of a static table. A fixed-length list communicates this constraint and can be more efficient. Other sequences grow and shrink at runtime: a shopping cart, a log of events, a filtered result. A growable list handles both. Dart lets you choose, and the choice affects what operations are allowed.
The merging problem. Combining lists is a common operation: prepending a header, appending a footer, concatenating results from multiple sources. Without spread, this requires addAll calls or + operators that create intermediate lists. Spread makes the combination a single literal.
The conditional-inclusion problem. A list sometimes needs an element only when a condition holds. Without collection-if, this requires building the list imperatively and adding conditionally. With collection-if and spread, the condition is part of the literal.
The nullable-iterable problem. A list sometimes comes from a nullable source, and spreading a null throws. The ...? operator tolerates the null and treats it as an empty iterable, which avoids a null check.
a. Creating lists
A list literal is written with square brackets and comma-separated elements. The element type is inferred from the values.
var numbers = [1, 2, 3, 4, 5];
var names = ['Alice', 'Bob', 'Carol'];
var mixed = [1, 'two', 3.0]; // List<Object>
An explicit type can be given with the angle bracket syntax:
var numbers = <int>[1, 2, 3];
var names = <String>['Alice', 'Bob'];
var empty = <int>[];
An empty list literal without a type is List<dynamic>, which is usually not what is intended. The type annotation or a typed empty list prevents this.
var wrong = []; // List<dynamic>
var right = <int>[]; // List<int>
Lists can be created from other iterables with List.from, List.of, or the toList() method on an iterable:
var fromIterable = List<int>.from([1, 2, 3]);
var copy = List<int>.of(numbers);
var fromRange = List<int>.generate(5, (i) => i * 2); // [0, 2, 4, 6, 8]
The List.generate constructor is useful for computed sequences:
var squares = List<int>.generate(5, (i) => (i + 1) * (i + 1));
// [1, 4, 9, 16, 25]
A list can also be created with filled:
var zeros = List<int>.filled(5, 0); // [0, 0, 0, 0, 0]
The filled constructor produces a fixed-length list by default, but a growable variant exists:
var zeros = List<int>.filled(5, 0, growable: true);
b. Fixed-length versus growable lists
Every list in Dart is either fixed-length or growable. The distinction determines which operations are allowed.
| Aspect | Fixed-length | Growable |
|---|---|---|
| Length | Set at creation, cannot change | Can grow and shrink |
add | Not allowed | Allowed |
remove | Not allowed | Allowed |
insert | Not allowed | Allowed |
[]= | Allowed (replace element) | Allowed |
length = n | Not allowed | Allowed |
| Default for literals | Growable | Growable |
Created with List.filled | Fixed (unless growable: true) | Growable |
| Performance | Slightly more efficient | Slightly more overhead |
The default for a list literal is growable:
var growable = [1, 2, 3];
growable.add(4); // OK
growable.remove(1); // OK
print(growable); // [2, 3, 4]
To create a fixed-length list, use List.filled without growable: true, or List with a length, or convert a growable list with toList(growable: false):
var fixed = List<int>.filled(3, 0); // fixed-length
var alsoFixed = List<int>.unmodifiable([1, 2, 3]);
var converted = growable.toList(growable: false);
A fixed-length list allows replacing elements but not adding or removing:
var fixed = List<int>.filled(3, 0);
fixed[0] = 10; // OK
fixed[1] = 20; // OK
// fixed.add(30); // Error: Cannot add to a fixed-length list
// fixed.removeAt(0); // Error
An unmodifiable list does not allow even replacement:
var immutable = List<int>.unmodifiable([1, 2, 3]);
// immutable[0] = 10; // Error: Cannot modify an unmodifiable list
The choice depends on the intent. If the sequence is fixed by nature, a fixed-length or unmodifiable list communicates that and prevents accidental modification. If the sequence grows and shrinks, a growable list is appropriate.
c. Common list operations
Growable lists support the full set of mutation operations:
| Operation | Method | Effect |
|---|---|---|
| Add one | add(element) | Append to end |
| Add many | addAll(iterable) | Append all |
| Insert | insert(index, element) | Insert at index |
| Insert many | insertAll(index, iterable) | Insert all at index |
| Remove by value | remove(element) | Remove first match |
| Remove by index | removeAt(index) | Remove at index |
| Remove last | removeLast() | Remove and return last |
| Remove range | removeRange(start, end) | Remove a range |
| Remove where | removeWhere(test) | Remove matching elements |
| Clear | clear() | Remove all |
| Sort | sort([compare]) | Sort in place |
| Shuffle | shuffle() | Randomize order |
| Reverse | reversed | Returns a reversed iterable |
var list = [1, 2, 3];
list.add(4); // [1, 2, 3, 4]
list.addAll([5, 6]); // [1, 2, 3, 4, 5, 6]
list.insert(0, 0); // [0, 1, 2, 3, 4, 5, 6]
list.remove(3); // [0, 1, 2, 4, 5, 6]
list.removeAt(0); // [1, 2, 4, 5, 6]
list.removeWhere((n) => n > 4); // [1, 2, 4]
list.sort(); // [1, 2, 4]
list.clear(); // []
Read-only operations:
| Operation | Method | Result |
|---|---|---|
| Length | length | Number of elements |
| Access | list[i] | Element at index |
| First | first | First element |
| Last | last | Last element |
| Contains | contains(element) | Whether the value exists |
| Index of | indexOf(element) | Index of first match |
| Sublist | sublist(start, [end]) | Copy of a range |
| Take | take(n) | First n elements |
| Skip | skip(n) | All but first n |
| Reversed | reversed | Reversed iterable |
var numbers = [10, 20, 30, 40, 50];
print(numbers.length); // 5
print(numbers[2]); // 30
print(numbers.first); // 10
print(numbers.last); // 50
print(numbers.contains(30)); // true
print(numbers.indexOf(30)); // 2
print(numbers.sublist(1, 3)); // [20, 30]
print(numbers.take(2)); // (10, 20)
print(numbers.skip(2)); // (30, 40, 50)
print(numbers.reversed); // (50, 40, 30, 20, 10)
The sublist method returns a new list. The take, skip, and reversed methods return lazy iterables, not lists. Call .toList() to materialize them.
var firstTwo = numbers.take(2).toList(); // [10, 20]
d. The spread operator (…)
The spread operator ... expands an iterable into the surrounding collection literal. It works in list, set, and map literals.
var a = [1, 2, 3];
var b = [4, 5, 6];
var combined = [...a, ...b];
print(combined); // [1, 2, 3, 4, 5, 6]
Spread can combine literals with variables:
var header = ['Name', 'Age'];
var rows = [['Alice', 30], ['Bob', 25]];
var table = [...header, ...rows];
// ['Name', 'Age', ['Alice', 30], ['Bob', 25]]
Spread can prepend or append elements:
var items = [2, 3, 4];
var withEdges = [1, ...items, 5];
print(withEdges); // [1, 2, 3, 4, 5]
Spread works with any Iterable, not just lists:
var range = List.generate(5, (i) => i); // [0, 1, 2, 3, 4]
var doubled = [...range, ...range.map((n) => n * 2)];
// [0, 1, 2, 3, 4, 0, 2, 4, 6, 8]
Spread also works in set and map literals:
var set = {1, 2, 3};
var combinedSet = {...set, 4, 5}; // {1, 2, 3, 4, 5}
var map1 = {'a': 1, 'b': 2};
var map2 = {'c': 3, 'd': 4};
var combinedMap = {...map1, ...map2};
// {'a': 1, 'b': 2, 'c': 3, 'd': 4}
For maps, spread copies the key-value pairs. If both maps have the same key, the later one wins.
e. The null-aware spread operator (…?)
Spreading a null iterable throws an error. The ...? operator tolerates null and treats it as an empty iterable.
List<int>? nullableList;
var result = [1, 2, ...?nullableList, 3];
print(result); // [1, 2, 3]
Without the ?, the same code throws:
var result = [1, 2, ...nullableList, 3]; // Error: null is not Iterable
The null-aware spread is useful when the iterable comes from a nullable source, such as a function that returns List<T>? or a field that may be unset.
class Config {
final List<String>? extraHosts;
Config({this.extraHosts});
List<String> get allHosts => [
'localhost',
...?extraHosts,
'example.com',
];
}
If extraHosts is null, the spread contributes nothing. If it is set, its elements are inserted.
The null-aware spread works in set and map literals as well:
var set = {1, 2, ...?nullableSet};
var map = {'a': 1, ...?nullableMap};
f. Combining spread with collection-if and collection-for
The spread operator composes with collection-if and collection-for, which are the declarative forms of conditional and iterative inclusion in a literal.
var items = [
'always',
if (includeExtra) 'extra',
for (var i = 0; i < 3; i++) 'item $i',
];
Collection-if includes an element only when a condition is true. Collection-for includes multiple elements generated by an iteration.
Spread can be combined with these:
var list = [
'header',
...?optionalItems,
if (includeFooter) 'footer',
for (var item in items) item.name,
];
This single literal expresses prepending a header, conditionally spreading optional items, conditionally including a footer, and mapping a collection to names. It is equivalent to a sequence of add and addAll calls, but declarative.
A common pattern is conditionally spreading a list:
var commands = [
'start',
if (verbose) '--verbose',
...?extraArgs,
'end',
];
The if (verbose) '--verbose' includes a single element conditionally. The ...?extraArgs includes all of a nullable list. Together they build the command list without imperative logic.
Another pattern is flattening a list of lists:
var matrix = [
[1, 2, 3],
[4, 5, 6],
];
var flat = [for (var row in matrix) ...row];
print(flat); // [1, 2, 3, 4, 5, 6]
The collection-for iterates over the rows, and the spread flattens each row into the result. This is the declarative form of a nested loop.
Complete Example Session
// ============================================
// PART 1: LIST LITERALS
// ============================================
void main() {
var numbers = [1, 2, 3, 4, 5];
var names = ['Alice', 'Bob'];
var typed = <int>[1, 2, 3];
var empty = <String>[];
print(numbers); // [1, 2, 3, 4, 5]
print(names); // [Alice, Bob]
}
// ============================================
// PART 2: GROWABLE LIST
// ============================================
void main() {
var list = [1, 2, 3];
list.add(4);
list.addAll([5, 6]);
print(list); // [1, 2, 3, 4, 5, 6]
list.remove(3);
list.removeAt(0);
print(list); // [2, 4, 5, 6]
}
// ============================================
// PART 3: FIXED-LENGTH LIST
// ============================================
void main() {
var fixed = List<int>.filled(3, 0);
fixed[0] = 10;
fixed[1] = 20;
fixed[2] = 30;
print(fixed); // [10, 20, 30]
// fixed.add(40); // Error: Cannot add to a fixed-length list
}
// ============================================
// PART 4: UNMODIFIABLE LIST
// ============================================
void main() {
var immutable = List<int>.unmodifiable([1, 2, 3]);
print(immutable); // [1, 2, 3]
// immutable[0] = 10; // Error: Cannot modify an unmodifiable list
}
// ============================================
// PART 5: LIST OPERATIONS
// ============================================
void main() {
var numbers = [10, 20, 30, 40, 50];
print(numbers.first); // 10
print(numbers.last); // 50
print(numbers.contains(30)); // true
print(numbers.indexOf(30)); // 2
print(numbers.sublist(1, 3)); // [20, 30]
print(numbers.take(2).toList()); // [10, 20]
print(numbers.skip(2).toList()); // [30, 40, 50]
print(numbers.reversed.toList()); // [50, 40, 30, 20, 10]
}
// ============================================
// PART 6: BASIC SPREAD
// ============================================
void main() {
var a = [1, 2, 3];
var b = [4, 5, 6];
var combined = [...a, ...b];
print(combined); // [1, 2, 3, 4, 5, 6]
var withEdges = [0, ...a, 4];
print(withEdges); // [0, 1, 2, 3, 4]
}
// ============================================
// PART 7: SPREAD IN SETS AND MAPS
// ============================================
void main() {
var set1 = {1, 2, 3};
var set2 = {3, 4, 5};
var combinedSet = {...set1, ...set2};
print(combinedSet); // {1, 2, 3, 4, 5}
var map1 = {'a': 1, 'b': 2};
var map2 = {'b': 3, 'c': 4};
var combinedMap = {...map1, ...map2};
print(combinedMap); // {a: 1, b: 3, c: 4}
}
// ============================================
// PART 8: NULL-AWARE SPREAD
// ============================================
void main() {
List<int>? nullable;
var result = [1, 2, ...?nullable, 3];
print(result); // [1, 2, 3]
nullable = [10, 20];
var result2 = [1, 2, ...?nullable, 3];
print(result2); // [1, 2, 10, 20, 3]
}
// ============================================
// PART 9: SPREAD WITH COLLECTION-IF
// ============================================
void main() {
var includeExtra = true;
var extra = ['x', 'y'];
var items = [
'start',
if (includeExtra) '--verbose',
...extra,
'end',
];
print(items); // [start, --verbose, x, y, end]
}
// ============================================
// PART 10: SPREAD WITH COLLECTION-FOR
// ============================================
void main() {
var matrix = [
[1, 2, 3],
[4, 5, 6],
];
var flat = [for (var row in matrix) ...row];
print(flat); // [1, 2, 3, 4, 5, 6]
}
These ten parts cover list literals, growable lists, fixed-length lists, unmodifiable lists, list operations, basic spread, spread in sets and maps, null-aware spread, spread with collection-if, and spread with collection-for.
Quick Reference
List Creation
| Form | Example |
|---|---|
| Literal | [1, 2, 3] |
| Typed literal | <int>[1, 2, 3] |
| Empty | <int>[] |
| From iterable | List<int>.from(iterable) |
| Copy | List<int>.of(other) |
| Generate | List<int>.generate(5, (i) => i * 2) |
| Filled | List<int>.filled(5, 0) |
| Unmodifiable | List<int>.unmodifiable([1, 2]) |
Fixed vs Growable
| Operation | Fixed | Growable |
|---|---|---|
| Replace element | Yes | Yes |
add | No | Yes |
remove | No | Yes |
insert | No | Yes |
clear | No | Yes |
length = n | No | Yes |
Common Operations
| Operation | Method |
|---|---|
| Add one | add |
| Add many | addAll |
| Insert | insert |
| Remove by value | remove |
| Remove by index | removeAt |
| Remove matching | removeWhere |
| Sort | sort |
| Reverse | reversed |
| Sublist | sublist |
| Contains | contains |
| Index | indexOf |
Spread Operators
| Operator | Meaning |
|---|---|
... | Expand iterable into literal |
...? | Expand or treat null as empty |
Spread Syntax
| Context | Example |
|---|---|
| List | [...a, ...b] |
| Set | {...a, ...b} |
| Map | {...a, ...b} |
| Nullable | [...?nullable] |
| With if | [if (cond) x, ...items] |
| With for | [for (x in items) ...x] |
Best Practices
✅ Do This:
// Use typed empty lists
var list = <int>[];
// Use spread to combine
var combined = [...a, ...b];
// Use null-aware spread for nullable
var result = [1, ...?nullable, 2];
// Use fixed-length for fixed data
var days = List<String>.unmodifiable(['Mon', 'Tue']);
// Use collection-for to flatten
var flat = [for (var row in matrix) ...row];
❌ Don’t Do This:
// Untyped empty list
var list = []; // ❌ List<dynamic>
// Spreading nullable without ?
var result = [...nullable]; // ❌ throws if null
// Using addAll when spread works
var combined = []; // ❌ use [...a, ...b]
combined.addAll(a);
combined.addAll(b);
// Mutating a fixed-length list
var fixed = List<int>.filled(3, 0); // ❌ add fails
fixed.add(1);
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
List<dynamic> inferred | Empty literal without type | Annotate the type |
add fails | Fixed-length list | Use a growable list |
| Spread throws | Nullable iterable | Use ...? |
take/skip not a list | Returns lazy iterable | Call .toList() |
| Map key overwritten | Duplicate keys in spread | Order determines winner |
| Modification during iteration | removeWhere in for-in | Collect then remove |
Real-World Examples
1. Combine Lists
var all = [...first, ...second];
2. Prepend and Append
var result = [header, ...items, footer];
3. Nullable List
var result = [...defaults, ...?optional];
4. Conditional Element
var args = ['run', if (verbose) '-v', ...extra];
5. Flatten Matrix
var flat = [for (var row in matrix) ...row];
6. Merge Maps
var config = {...defaults, ...overrides};
7. Remove Duplicates
var unique = {...list}.toList();
8. Fixed Lookup Table
var hex = List<String>.unmodifiable(
['0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'a', 'b', 'c', 'd', 'e', 'f'],
);
9. Generate Range
var range = List<int>.generate(10, (i) => i + 1);
10. Build Command List
var command = [
'docker', 'run',
if (detached) '-d',
'--name', name,
...?extraFlags,
image,
];
Visual
Fixed vs Growable
┌──────────────────────────────────────────────────────────────┐
│ GROWABLE (default literal): │
│ var list = [1, 2, 3]; │
│ ┌────┬────┬────┐ ┌────┬────┬────┬────┐ │
│ │ 1 │ 2 │ 3 │ → │ 1 │ 2 │ 3 │ 4 │ add(4) │
│ └────┴────┴────┘ └────┴────┴────┴────┘ │
│ Can grow and shrink │
│ │
│ FIXED-LENGTH: │
│ var fixed = List<int>.filled(3, 0); │
│ ┌────┬────┬────┐ │
│ │ 0 │ 0 │ 0 │ Cannot add or remove │
│ └────┴────┴────┘ │
│ Can replace elements: fixed[0] = 10 │
│ │
│ UNMODIFIABLE: │
│ var imm = List<int>.unmodifiable([1, 2, 3]); │
│ Cannot replace or add or remove │
└──────────────────────────────────────────────────────────────┘
Spread Operator
┌──────────────────────────────────────────────────────────────┐
│ var a = [1, 2, 3]; │
│ var b = [4, 5, 6]; │
│ │
│ var combined = [...a, ...b]; │
│ │ │
│ ▼ │
│ [1, 2, 3, 4, 5, 6] │
│ │
│ Spread expands the iterable into the surrounding literal. │
└──────────────────────────────────────────────────────────────┘
Null-Aware Spread
┌──────────────────────────────────────────────────────────────┐
│ List<int>? nullable; │
│ │
│ [...nullable] → throws: null is not Iterable │
│ [...?nullable] → [] (treats null as empty) │
│ │
│ var result = [1, ...?nullable, 2]; │
│ → [1, 2] │
│ │
│ nullable = [10, 20]; │
│ var result = [1, ...?nullable, 2]; │
│ → [1, 10, 20, 2] │
└──────────────────────────────────────────────────────────────┘
Spread with Collection-If and Collection-For
┌──────────────────────────────────────────────────────────────┐
│ var items = [ │
│ 'start', │
│ if (verbose) '--verbose', │
│ ...extra, │
│ for (var n in range) 'item $n', │
│ 'end', │
│ ]; │
│ │
│ Each construct contributes elements: │
│ - literal: 'start', 'end' │
│ - collection-if: '--verbose' when verbose │
│ - spread: all elements of extra │
│ - collection-for: 'item 0', 'item 1', ... │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| List literal | [1, 2, 3] |
| Typed list | <int>[1, 2, 3] |
| Growable | Default for literals |
| Fixed-length | List.filled(n, value) |
| Unmodifiable | List.unmodifiable(...) |
| Add | add, addAll (growable only) |
| Remove | remove, removeAt, removeWhere (growable only) |
| Spread | ...iterable |
| Null-aware spread | ...?nullableIterable |
| Spread in map | {...a, ...b} |
| Collection-if | [if (cond) x] |
| Collection-for | [for (x in items) x] |
Key takeaways:
- Lists are ordered and indexed from zero. They are the default collection for sequences, and the literal syntax
[...]creates a growable list. - Fixed-length and growable lists differ in what they allow. A fixed-length list can replace elements but not add or remove. A growable list supports the full set of mutation operations. Unmodifiable lists prevent even replacement.
- Typed empty lists prevent
List<dynamic>.[]isList<dynamic>;<int>[]isList<int>. Annotating the type catches mistakes at compile time. - The spread operator expands an iterable into a literal.
[...a, ...b]combines two lists. It works in list, set, and map literals, and with anyIterable, not just lists. - The null-aware spread tolerates null.
...?nullabletreats a null iterable as empty, avoiding the error that plain spread would throw. - Spread composes with collection-if and collection-for. A list literal can conditionally include elements and iterate over collections, all in one declarative expression.
take,skip, andreversedreturn lazy iterables. They do not produce lists until.toList()is called.sublistreturns a new list directly.- Merging maps with spread overwrites duplicates. The later spread wins for duplicate keys, which is useful for defaults and overrides:
{...defaults, ...overrides}.
Remember: Lists are the workhorse of Dart collections, and the fixed-versus-growable distinction is the first decision when creating one. A growable list is the default and supports the full set of mutation operations. A fixed-length list communicates that the size is final, and an unmodifiable list prevents any change. The spread operator makes list combination declarative: [...a, ...b] replaces a sequence of addAll calls. The null-aware spread handles nullable sources without a null check. Together with collection-if and collection-for, spread makes list literals expressive enough to build complex sequences — command arguments, widget children, configuration values — in a single expression that reads like a specification rather than a procedure.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!