| |

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.

AspectFixed-lengthGrowable
LengthSet at creation, cannot changeCan grow and shrink
addNot allowedAllowed
removeNot allowedAllowed
insertNot allowedAllowed
[]=Allowed (replace element)Allowed
length = nNot allowedAllowed
Default for literalsGrowableGrowable
Created with List.filledFixed (unless growable: true)Growable
PerformanceSlightly more efficientSlightly 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:

OperationMethodEffect
Add oneadd(element)Append to end
Add manyaddAll(iterable)Append all
Insertinsert(index, element)Insert at index
Insert manyinsertAll(index, iterable)Insert all at index
Remove by valueremove(element)Remove first match
Remove by indexremoveAt(index)Remove at index
Remove lastremoveLast()Remove and return last
Remove rangeremoveRange(start, end)Remove a range
Remove whereremoveWhere(test)Remove matching elements
Clearclear()Remove all
Sortsort([compare])Sort in place
Shuffleshuffle()Randomize order
ReversereversedReturns 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:

OperationMethodResult
LengthlengthNumber of elements
Accesslist[i]Element at index
FirstfirstFirst element
LastlastLast element
Containscontains(element)Whether the value exists
Index ofindexOf(element)Index of first match
Sublistsublist(start, [end])Copy of a range
Taketake(n)First n elements
Skipskip(n)All but first n
ReversedreversedReversed 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

FormExample
Literal[1, 2, 3]
Typed literal<int>[1, 2, 3]
Empty<int>[]
From iterableList<int>.from(iterable)
CopyList<int>.of(other)
GenerateList<int>.generate(5, (i) => i * 2)
FilledList<int>.filled(5, 0)
UnmodifiableList<int>.unmodifiable([1, 2])

Fixed vs Growable

OperationFixedGrowable
Replace elementYesYes
addNoYes
removeNoYes
insertNoYes
clearNoYes
length = nNoYes

Common Operations

OperationMethod
Add oneadd
Add manyaddAll
Insertinsert
Remove by valueremove
Remove by indexremoveAt
Remove matchingremoveWhere
Sortsort
Reversereversed
Sublistsublist
Containscontains
IndexindexOf

Spread Operators

OperatorMeaning
...Expand iterable into literal
...?Expand or treat null as empty

Spread Syntax

ContextExample
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

PitfallWhy It HappensFix
List<dynamic> inferredEmpty literal without typeAnnotate the type
add failsFixed-length listUse a growable list
Spread throwsNullable iterableUse ...?
take/skip not a listReturns lazy iterableCall .toList()
Map key overwrittenDuplicate keys in spreadOrder determines winner
Modification during iterationremoveWhere in for-inCollect 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

ItemValue
List literal[1, 2, 3]
Typed list<int>[1, 2, 3]
GrowableDefault for literals
Fixed-lengthList.filled(n, value)
UnmodifiableList.unmodifiable(...)
Addadd, addAll (growable only)
Removeremove, 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>. [] is List<dynamic>; <int>[] is List<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 any Iterable, not just lists.
  • The null-aware spread tolerates null. ...?nullable treats 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, and reversed return lazy iterables. They do not produce lists until .toList() is called. sublist returns 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!