Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Dart (3.13)  Module 5 – Collections and iterables

Collection if, for and spread in Dart

Build Dart lists, sets and maps with collection if, for and spread, use null-aware elements from 3.8, and know when a const literal is a shared object.

Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Build lists, sets and maps with collection if, collection for and nested elements
  • Merge another collection with spread and skip a null source with null-aware spread
  • Remove a null element or map entry with the null-aware element added in Dart 3.8
  • Write const collections and recognise when two of them share one object
  • Choose the type of a bare literal with a context or an explicit type argument

Before you start

What you’ll learn · Before you start

What you will learn

  • Build lists, sets and maps with collection if, collection for and nested elements
  • Merge another collection with spread and skip a null source with null-aware spread
  • Remove a null element or map entry with the null-aware element added in Dart 3.8
  • Write const collections and recognise when two of them share one object
  • Choose the type of a bare literal with a context or an explicit type argument

Before you start

On this page

A side menu for a shop app has a fixed order and three choices to make at each page load: whether the cart has anything in it, whether the person is signed in and what the categories are today. Writing those choices as menu.add(...) calls spreads the structure across eight lines of code; Dart lets you say it in one list literal instead.

You will first put control flow inside a list literal with collection if and collection for, then inline another collection with spread and skip a null source with null-aware spread. The null-aware element added in Dart 3.8 drops a null from a list or a map in one character. The last two sections turn a const literal into a shared, deeply immutable object and show when element-type inference needs a nudge.

A collection if adds a piece only when a condition is true

Dart lets a list, set or map literal hold more than values and MapEntrys: it also accepts collection if: An if element inside a Dart list, set or map literal. It writes zero or one element into the surrounding collection and may carry an else branch, so one literal can turn a piece on or off from the data. and collection for: A for element inside a Dart list, set or map literal, with the shape for (var x in xs) e or a C-style for. It walks the loop and adds the element its body builds to the surrounding collection, zero or more times. as elements of its own. A collection if adds an element when its condition is true, drops it when the condition is false, and may carry an else with a second element. This program builds the side menu of a shop app in one literal, and prints it for a guest and for a signed-in user with three items in her cart:

menu.dart: a side menu built as one literal Dart · menu.dart
// menu.dart (Dart 3.13.5): a side-menu for a shop app, built as one list
// literal. 'Settings' and 'Sign out' appear only for signed-in users, and
// every category of the shop becomes a tile through a collection for.
void main() {
  // Pretend input: a non-signed-in guest, and a signed-in user with a cart.
  final categories = <String>['Daily staples', 'Fruit', 'Dairy', 'Household'];

  List<String> menuFor({required bool signedIn, int cartItems = 0}) {
    return <String>[
      'Home',
      for (final c in categories) 'Shop: $c',
      if (cartItems > 0) 'Cart ($cartItems)' else 'Cart',
      if (signedIn) 'Settings',
      if (signedIn) 'Sign out' else 'Sign in',
      'About',
    ];
  }

  final guest = menuFor(signedIn: false);
  final asha = menuFor(signedIn: true, cartItems: 3);

  print('Guest menu (${guest.length} items):');
  for (final item in guest) {
    print('  $item');
  }
  print('');
  print('Asha, signed in with 3 items in her cart (${asha.length} items):');
  for (final item in asha) {
    print('  $item');
  }
}

Output

Guest menu (8 items):
  Home
  Shop: Daily staples
  Shop: Fruit
  Shop: Dairy
  Shop: Household
  Cart
  Sign in
  About

Asha, signed in with 3 items in her cart (9 items):
  Home
  Shop: Daily staples
  Shop: Fruit
  Shop: Dairy
  Shop: Household
  Cart (3)
  Settings
  Sign out
  About

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run menu.dart

Read the output against the literal. The guest sees eight items; Asha sees nine, three of them different. The source did not grow: the choices did. Step through the literal to see each piece decide for itself:

Interactive · step by step One literal grows into the guest's eight-item menu Step through the menu literal of menu.dart and watch each piece decide what joins the list.
One literal grows into the guest's eight-item menu, step 8 of 8. Showing return [, 'Home', for (c in categories), if cart has items (cart: 3, Cart (3) added), if signed in (signedIn: true, add), if signed in else... (signedIn: true, Sign out), 'About', Home, Shop: 4 items, Cart, Sign in, About. In focus: if cart has items, if signed in, if signed in else....return ['Home'for (c in categories)if cart has itemscart: 3, Cart (3) addedif signed insignedIn: true, addif signed in else...signedIn: true, Sign out'About'HomeShop: 4 itemsone per categoryCartSign inAbout

The drawing shows the whole scene; its 8 steps are listed below.

Every step as text
  1. The literal starts empty. menuFor is called with signedIn: false and cartItems: 0. Dart opens an empty List<String> and will drop or add each piece of the literal, in order, from top to bottom.
  2. A plain string joins the list. The first piece is the ordinary expression 'Home'. Nothing decides whether it belongs: Dart simply adds it, so the list now has one item.
  3. A for repeats one piece. for (final c in categories) 'Shop: $c' is a collection for: Dart walks the four categories and adds one 'Shop: ...' string each time. The literal has one line, and the list grows by four items.
  4. An if chooses between two. if (cartItems > 0) 'Cart (3)' else 'Cart' adds exactly one item: Dart checks cartItems > 0. For the guest it is 0, so the else branch runs and 'Cart' joins the list.
  5. An if with no else drops it. if (signedIn) 'Settings' is a collection if with no else. For the guest signedIn is false, so this piece is dropped and the list length does not change.
  6. if-else picks the sign-in label. if (signedIn) 'Sign out' else 'Sign in' adds one of the two. signedIn is still false, so 'Sign in' joins the list, which is now seven items long.
  7. A last string closes it. The last piece is the plain string 'About', always added. The guest's menu is eight items long, in exactly the order of the literal, and the whole literal evaluated once.
  8. Signed in: two more items. A second call with signedIn: true, cartItems: 3 goes through the same literal: 'Cart (3)' wins the first if, 'Settings' is added, and 'Sign out' replaces 'Sign in', so the list is nine items long. The source does not grow, only the choices change.

Three ideas sit inside this literal:

  • A plain string, 'Home', is an expression element and always joins the list.
  • A collection for, for (final c in categories) 'Shop: $c', adds one element per step of its loop. The Dart documentation says a for element “iterates and repeatedly evaluates a given inner element, inserting zero or more resulting values”. With four categories, this one line added four items.
  • A collection if, if (signedIn) 'Settings', adds its element when the condition is true. The two-branch form, if (cartItems > 0) 'Cart (3)' else 'Cart', always adds exactly one of the two.

The Dart specification treats these as a kind of element too, so they nest inside each other: for (final c in categories) if (c.startsWith('Fruit')) 'Fruit shop' adds one tile only when the loop reaches the fruit category. The Flutter widget tree is a List<Widget>, and this is how a widget list usually turns options on and off: no builder method, no extra variable for a half-built list.

Reaching for add inside the literal

Writing [... if (signedIn) menu.add('Settings')] is a double mistake. add changes a list in place and returns void, so the literal tries to splice a void into itself and the compiler refuses it. Keep the branch inside the literal (if (signedIn) 'Settings'); let the literal decide what joins it.

The same literal becomes testable in one go: a function that returns it takes the two inputs the choices need (signedIn and cartItems), and tests pass the pairs they want to check.

Try it

Change signedIn to true and add if (cartItems == 0) 'Cart is empty' else 'Cart ($cartItems)' instead of the current line. The guest’s menu now says exactly what the cart is doing, in words.

The language specification groups collection if, collection for and spread under the name “control-flow elements” and allows them to nest inside each other, so richer rules compose without turning the literal into a function.

Key idea

Collection if and collection for are elements of a literal, not statements that return one: write them in place and let the literal choose what goes into the list, the set or the map.

Checkpoint

Check: Collection if and for inside a literal

  1. Question 1 of 2 menuFor is called with signedIn: false and cartItems: 0 in menu.dart. How many items are in the list the literal returns?

    Type a number.

    A hint

    Each plain string is one item; collection for adds one per step; collection if adds zero or one.

    Show the answer: Question 1 of 2

    Answer: 8 items

    One for 'Home', four for the collection for (one tile per category), one for the else branch 'Cart', zero for the drop-only if before 'Settings', one for the else branch 'Sign in', and one for 'About': eight items in all, as the guest-menu block of the recorded output showed.

  2. Question 2 of 2 Which line builds the list [1, 4, 9, 16, 7], a square of each number followed by 7, with a collection for?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: var items = [1, for (var n in numbers) n * n, 7];

    A for element iterates and repeats a given inner expression, as the Dart documentation puts it, so one line of the literal adds four items in order between '1' and '7'.

Spread inlines another collection, and skips a null source

A spread element: An ... followed by an Iterable, written inside a Dart list, set or map literal. It inserts every value of the source into the surrounding collection, in order, so one literal can build on another., ...xs, inserts every value of another Iterable into the surrounding collection, in order. Spread works in lists, sets and maps alike (for a map, the source has to be a map, so its entries can be spread). The null-aware spread: A ...? followed by a nullable collection, inside a Dart list, set or map literal. A null source is treated as empty: no element is inserted, and no guard is needed before the spread., ...?xs, does the same but treats null as an empty source; without it, spreading a nullable value is a compile-time error. This program builds a query-parameter map from a few optional fields, mixing spread, null-aware spread, collection if and the null-aware map entries added in Dart 3.8:

query_params.dart: query parameters with spread and null-aware entries Dart · query_params.dart
// query_params.dart (Dart 3.13.5): building a query-parameter map for a
// search request from optional fields, with spread, null-aware spread and
// the null-aware map entries added in 3.8.
void main() {
  // Two defaults every call should carry, then per-request overrides.
  const defaults = <String, String>{
    'format': 'json',
    'page_size': '20',
  };

  Map<String, String> search({
    String? q,
    int? page,
    List<String>? tags,
    Map<String, String>? extra,
  }) {
    return <String, String>{
      ...defaults,
      ...?extra,
      'q': ?q,
      if (page != null) 'page': '$page',
      if (tags != null && tags.isNotEmpty) 'tags': tags.join(','),
    };
  }

  final empty = search();
  final guest = search(q: 'moong dal');
  final asha = search(
    q: 'moong dal',
    page: 3,
    tags: <String>['organic', 'india'],
    extra: <String, String>{'sort': 'price', 'page_size': '40'},
  );

  print('Empty search:  $empty');
  print('Guest search:  $guest');
  print('Asha search:   $asha');

  // The same literal, with every optional field null, keeps only the defaults.
  print('All null:      ${search(q: null, page: null, tags: null)}');
}

Output

Empty search:  {format: json, page_size: 20}
Guest search:  {format: json, page_size: 20, q: moong dal}
Asha search:   {format: json, page_size: 40, sort: price, q: moong dal, page: 3, tags: organic,india}
All null:      {format: json, page_size: 20}

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run query_params.dart

The four printed lines show which rules fired:

  • Empty search has only the two defaults, because every optional field was null and every collection if saw null.
  • Guest search adds q: 'moong dal' through the null-aware map entry 'q': ?q. The entry copies q only when it is not null, so a String? value fits a Map<String, String> by exactly the field whose value is missing.
  • Asha search spreads extra with ...?extra. extra holds 'page_size': '40' and 'sort': 'price', which land after the defaults, so the later value for page_size wins and the final map carries 40. The collection if for page runs because page is 3, and the tags branch runs because the list has values; both line up with the specification’s “Insert the value into the surrounding collection”.
  • The last line passes q: null, page: null, tags: null on purpose. Every optional branch drops out, and the result is the empty-search output again. One literal, one shape, every missing input gracefully silent.

Inside a list or a set, a null-aware element is one character: ?maybeNumber adds maybeNumber only when it is not null, with its non-nullable type. The same three sources, written with and without the ?, make the difference visible:

null_aware_elements.dart: ?value against a plain value Dart · null_aware_elements.dart
// A null-aware element inside a list literal: ?value adds value only when it is not null.
// Compare the elements written with ? against the one written without it.
void main() {
  int? absentValue = null;
  int? presentValue = 3;

  final items = [1, ?absentValue, ?presentValue, absentValue, 5];
  print('items: $items');
  print('length: ${items.length}');

  // The same three sources in a set and in a map value.
  final tags = <int>{1, ?absentValue, ?presentValue};
  print('tags: $tags');

  final row = <String, int>{'first': 1, 'second': ?presentValue, 'third': ?absentValue};
  print('row: $row');
  print('row has third: ${row.containsKey('third')}');
}

Output

items: [1, 3, null, 5]
length: 4
tags: {1, 3}
row: {first: 1, second: 3}
row has third: false

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run null_aware_elements.dart

The list came out as [1, 3, null, 5] with a length of 4: ?absentValue added nothing, ?presentValue added 3, and only the fourth element, written without ?, kept its null. The map behaves the same way one level down: 'third': ?absentValue left the key out altogether, so containsKey('third') is false rather than a key whose value is null.

Spreading a nullable collection as if it were ordinary

List<int>? maybe; var items = [...maybe]; is a compile-time error, because maybe could be null. The fix is one character: write ...?maybe and maybe as null is treated as empty, so no element is inserted.

Spread also drops a common dead-end: linking the pieces into a bigger list with +. A spread of [...a, ...b] runs once and allocates one list; two + calls allocate two lists, the first a throwaway. The List class says + “returns the concatenation of this list and other”, so the extra list is unavoidable. For a lazy walk over several sources without building anything, the lazy Iterable API has followedBy and expand.

Key idea

Spread, null-aware spread and null-aware elements let one literal carry the shape of the collection and defer every optional piece until the data decides: no builder, no half-built list, no extra object.

Checkpoint

Check: Spread and null-aware spread

  1. Question 1 of 2 "int? absent = null; int? present = 3; var items = [1, ?absent, ?present, absent, 5];" What is in items?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: [1, 3, null, 5]

    '?x' is a null-aware element: it adds the value only when it is not null, with its non-nullable type. The Dart language documentation shows this example word for word; the result is [1, 3, null, 5].

  2. Question 2 of 2 "List<int>? extra; var items = [1, 2, ...extra, 3];" compiles with an error. Which change makes it build the list [1, 2, 3] when 'extra' is null?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Replace "...extra" with "...?extra"

    Dart refuses "...value" when 'value' may be null; the two-character "...?" tells Dart that a null source adds no elements. The language documentation calls that out explicitly.

Const makes the literal a shared, deeply immutable object

A const collection is a compile-time constant: A value the compiler can build before the program runs, from literals, other constants, arithmetic on constant numbers and const constructors. Dart declares one with const, and no part of it can ever change. More in “Variables: var, final, const and type inference”: Dart builds it before the program runs and keeps it as a canonical constant: The single object Dart keeps for each constant value: two const expressions that build equal values give the same object, so identical() on them returns true. More in “Variables: var, final, const and type inference”, so two const expressions that build equal values share one object. This program makes three pairs of names and asks identical which of them point to the same list:

const_identity.dart: when two names share one list Dart · const_identity.dart
// const_identity.dart (Dart 3.13.5): two const literals that build equal
// values share one object, while two non-const literals do not. A const
// literal with spreads is still canonical when every piece is constant.
void main() {
  const groceries1 = <String>['bread', 'milk', 'eggs'];
  const groceries2 = <String>['bread', 'milk', 'eggs'];

  final boughtOnMonday = const <String>['bread', 'milk', 'eggs'];
  final boughtOnFriday = const <String>['bread', 'milk', 'eggs'];

  final writtenOnMonday = <String>['bread', 'milk', 'eggs'];
  final writtenOnFriday = <String>['bread', 'milk', 'eggs'];

  print('const declaration twice:       ${identical(groceries1, groceries2)}');
  print('const literal in a final twice: '
      '${identical(boughtOnMonday, boughtOnFriday)}');
  print('plain literal twice:            '
      '${identical(writtenOnMonday, writtenOnFriday)}');

  // All three hold equal items; only identity differs.
  print('every list equals the first:   '
      '${groceries1.toString() == writtenOnMonday.toString()}');

  // A const literal built from other const literals with a spread is also
  // canonical: two const spreads of the same const base share one object.
  const base = <int>[1, 2, 3];
  const withFour = <int>[...base, 4];
  const withFourAgain = <int>[...base, 4];
  print('const spread of const base:    '
      '${identical(withFour, withFourAgain)}');
}

Output

const declaration twice:       true
const literal in a final twice: true
plain literal twice:            false
every list equals the first:   true
const spread of const base:    true

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run const_identity.dart

Its five lines are three answers and one proof. Two const declarations with the same shape share one object, and so do two const [...] expressions written in final holders; two plain literals do not. All three hold equal items, as the every list equals the first: true line shows, so the choice is between identical, which compares reference identity, and ==, which compares contents. Walk through what Dart actually keeps:

Interactive · step by step Two names, one or two lists Step through const and plain literals and watch how many list objects Dart really keeps.
Two names, one or two lists, step 4 of 4. Showing groceries1, groceries2, List: bread, milk, eggs, boughtOnMonday, boughtOnFriday, List: bread, milk, eggs (the same canonical list), writtenOnMonday, writtenOnFriday, List: bread, milk, eggs, List: bread, milk, eggs. In focus: List: bread, milk, eggs, List: bread, milk, eggs.groceries1groceries2List: bread, milk, eggsboughtOnMondayboughtOnFridayList: bread, milk, eggsthe same canonical listwrittenOnMondaywrittenOnFridayList: bread, milk, eggsList: bread, milk, eggs

The drawing shows the whole scene; its 4 steps are listed below.

Every step as text
  1. Two const names, one object. const groceries1 and const groceries2 both hold the same list literal. The compiler keeps one canonical object for each const value, so both names point to it and identical() returns true.
  2. const inside final: the same. boughtOnMonday and boughtOnFriday store the result of two const [...] expressions. The expression is a constant too, so canonicalisation finds the same object as the one above the names already know.
  3. Plain literals: two objects. writtenOnMonday and writtenOnFriday hold ordinary [...] literals, each built every time the line runs. The two lists hold equal items, but they are different objects, so identical() is false even though == for the items would be true.
  4. Deeply immutable: safe to share. Dart may share const lists only because they are deeply immutable: a const list, every item inside it and the items of any nested const list all refuse change at run time. Two names can hold one object without one path quietly changing what the other reads.

A const collection is also deep immutability: The guarantee that nothing inside a value ever changes, however many layers deep. A const collection in Dart is deeply immutable: its length, its elements and any const collections inside them all refuse change at run time.: nothing inside it ever changes, which is the only reason sharing one object is safe. Any attempt to grow, shrink or replace an element throws at run time, with Dart’s own words for what the thing is:

const_fails.dart: changing a const list throws Dart · const_fails.dart
// const_fails.dart (Dart 3.13.5): a const list is deeply immutable. Reading
// is fine; the smallest change throws at run time, as this program shows.
void main() {
  const groceries = <String>['bread', 'milk', 'eggs'];

  // Reading is fine.
  print('Length: ${groceries.length}, first: ${groceries.first}');

  // Any attempt to change it throws. The error text names what Dart stores
  // the const list as, an unmodifiable list.
  groceries.add('rice');
}

Output (exit status 255)

Length: 3, first: bread

Printed as an error (standard error)

Unhandled exception:
Unsupported operation: Cannot add to an unmodifiable list
#0      UnmodifiableListMixin.add (dart:_internal/list.dart:112:5)
#1      main (const_fails.dart:11:13)
#2      _delayEntrypointInvocation.<anonymous closure> (dart:isolate-patch/isolate_patch.dart:313:19)
#3      _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:192:12)

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run const_fails.dart

The last three lines of its output end with Unsupported operation: Cannot add to an unmodifiable list. Reading the first element, counting the length and iterating still work; only writes are refused.

A const literal is also strict about what sits inside. Every item has to be constant too (a literal number, a string, another constant, or a const constructor call), and a non-constant inside is a compile-time error (the message reads Error: Not a constant expression.). A spread inside a const works only when its source is also constant, as the withFour and withFourAgain lines of the first program show.

When to reach for const

Use const for a set of allowed values a program checks against (a list of currency codes, a map of error messages), the default state of a page or widget, and compile-time tables such as a lookup from a code to a label. The compiler keeps one copy, the compiler enforces that nothing changes, and tests do not have to defend against someone reassigning it.

For a cache key, a widget key, or a value tested with identical across the program, the sharing is the point. Two parts of the program that build the same key return one object; the look-up finds the same slot every time.

Key idea

const collections are canonical and deeply immutable: equal const literals share one object, every item inside must be constant, and the smallest write throws at run time.

Checkpoint

Check: Const collections and canonical identity

  1. Question 1 of 2 "const a = [1, 2, 3]; const b = [1, 2, 3]; final c = [1, 2, 3]; final d = [1, 2, 3];" Which identical() calls return true?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: identical(a, b) is true; identical(c, d) is false

    The example const_identity.dart printed 'true' for the two const names and 'false' for the two plain names, matching the Dart documentation's rule that equal const values are a single shared object.

  2. Question 2 of 2 const g = ['bread', 'milk']; g.add('eggs'); What happens when the second line runs?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The program throws 'Unsupported operation: Cannot add to an unmodifiable list' and ends

    const_fails.dart ran this very code and printed the recorded error text, which gives you the words to search for when a bug report arrives.

A bare literal picks its type from context, or from you

Dart’s static analyser picks the element type of a literal from how it is used. Written on their own, [] and {} have no information to work with, so a bare [] is a List<dynamic> and a bare {} is a map with dynamic keys and values. The moment the literal has to fit a target, inference uses the target. This program asks each literal what it became:

infer_types.dart: where a literal's type comes from Dart · infer_types.dart
// Where a collection literal's element type comes from: the context, an explicit type
// argument, or the items themselves. runtimeType prints what Dart settled on.
void main() {
  List<String> cities = [];
  final empty = <String>[];
  final numbers = [1, 2, 3];
  final mixed = <num>[1, 1.5, 2];
  Map<String, int> scores = {};
  final tags = <String>{};

  print('List<String> cities = []   -> ${cities.runtimeType}');
  print('final empty = <String>[]   -> ${empty.runtimeType}');
  print('final numbers = [1, 2, 3]  -> ${numbers.runtimeType}');
  print('final mixed = <num>[…]     -> ${mixed.runtimeType}');
  print('Map<String, int> scores={} -> ${scores.runtimeType}');
  print('final tags = <String>{}    -> ${tags.runtimeType}');

  // No context at all: the literal falls back to dynamic.
  final bare = [];
  final braces = {};
  print('final bare = []            -> ${bare.runtimeType}');
  print('final braces = {}          -> ${braces.runtimeType}');

  // A spread takes the element type of its source, not of the items it happens to hold.
  final names = <String>['Asha', 'Bala'];
  final objects = <Object>['Asha', 'Bala'];
  print('[...names]                 -> ${[...names].runtimeType}');
  print('[...objects]               -> ${[...objects].runtimeType}');
}

Output

List<String> cities = []   -> List<String>
final empty = <String>[]   -> List<String>
final numbers = [1, 2, 3]  -> List<int>
final mixed = <num>[…]     -> List<num>
Map<String, int> scores={} -> _Map<String, int>
final tags = <String>{}    -> _Set<String>
final bare = []            -> List<dynamic>
final braces = {}          -> _Map<dynamic, dynamic>
[...names]                 -> List<String>
[...objects]               -> List<Object>

Recorded with Dart SDK 3.13.5 (stable) on macos_arm64 on macOS 26 arm64. To run it yourself: dart run infer_types.dart

Read the right-hand column: a target type (List<String> cities = []) and an explicit type argument (<String>[]) both give List<String>, items alone give List<int>, and a literal with no context at all gives List<dynamic> or a dynamic map. The two map and set lines print _Map and _Set rather than Map and Set, because runtimeType names the class the SDK instantiated; the type arguments beside it are what inference settled on.

Three cases need a nudge:

  • A bare [] where the context does not say what the element type is: the right fix is <String>[] or whichever element type you mean, so later code is checked against it. Returning [] from a function whose return type is List<String> does what you expect; a top-level final empty = []; does not.
  • An empty literal with braces: {} is a Map, not a Set. A set of strings is <String>{}.
  • A literal built mostly through a spread: [...names] takes its element type from names, so a spread of List<String> gives List<String> without any angle brackets. A spread of List<Object> gives List<Object>, even when every item is a string, so the type of the source matters.

A List is also covariant, which the lists lesson covers in its sort and copy section: a List<Object> can hold a List<int>, which is fine for reading and surprises a line that writes into it. For a literal that will be read and written, write the element type you mean on the literal itself; the lint always_specify_types can enforce it.

Checkpoint

Check: Element-type inference of a literal

  1. One question Which of these empty literals has type Set<String>?

    Choose one answer.

    Show the answer: One question

    Answer: <String>{}

    The lesson's small table matched this up: a bare '{}' is a Map, and a Set of strings needs '<String>{}'. Dart prefers Map for an ambiguous empty literal, so writing the element type is the only way to say Set.

Practise: a breadcrumbs builder and a query-string helper

In a new console project, add the two functions below. Build each result in one literal: treat add, addAll, insert and remove on lists and maps as off limits, so the literal has to carry every decision.

  1. List<String> breadcrumbs({required String track, String? module, String? lesson, bool showHome = true}) returns the trail of names a learner sees above a lesson title. showHome: true adds 'Home' first; the track name always follows; the module and the lesson join the trail only when they are not null, in that order. Think of a reader on a mobile that falls back to a lighter page: the function should work when every field but track is null.
  2. Map<String, String> queryParams({String? q, int? page, List<String>? tags}) returns the parameters a search request should carry. Each field appears only when it has a value; tags becomes a comma-separated string. The result is always a Map<String, String>, so the request builder can read it straight into a URL.

Make a console project with dart create -t console literals_lab and check each function against the happy case and one empty case: all-null input, an empty tags list, and the Hindi menu of this lesson’s own figure.

Then work through the lesson’s self-check; the idea you know least comes first:

Practice that adapts to you

Practise this lesson

9 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 9 A Flutter widget needs a list of children: a title, every category widget, a Buy button only when the cart is non-empty, and a FAQ widget. Which collection-literal form is the ordinary way to write it?

    Choose one answer.

    Show the answer: Question 1 of 9

    Answer: [Title(), for (final c in categories) Tile(c), if (cart.isNotEmpty) BuyButton(), Faq()]

    One literal, one shape, each line a reason the item is there. The Dart documentation describes this very style for widget lists, and the menu example of the lesson is one running copy of it.

  2. Question 2 of 9 What does this print?

    Read the code, then choose one answer.

    var numbers = [1, 2, 3, 4, 5, 6];
    var items = [
      0,
      for (var n in numbers) if (n.isEven) n,
      7,
    ];
    print(items);
    Show the answer: Question 2 of 9

    Answer: [0, 2, 4, 6, 7]

    Collection for and collection if nest, as the Dart documentation shows. The for element runs six times, and the if inside it keeps three of them, giving [0, 2, 4, 6, 7].

  3. Question 3 of 9 "const defaults = {'page_size': '20'}; var p = {...defaults, 'page_size': '40'};" What is p['page_size']?

    Type a number.

    A hint

    Later entries overwrite earlier ones in a map literal.

    Show the answer: Question 3 of 9

    Answer: 40

    A map literal takes its entries in source order, so the second 'page_size' overwrites the first; the recorded output of query_params.dart showed the same rule when 'extra' overrode the defaults.

  4. Question 4 of 9 String? q; var m = {'format': 'json', 'q': ?q}; print(m); — what does this print?

    Choose one answer.

    Show the answer: Question 4 of 9

    Answer: {format: json}

    The null-aware value entry, added in Dart 3.8, lets a nullable field slot into a map whose value type refuses null. The query_params.dart output printed '{format: json, page_size: 20}' when both q and the extras were null.

  5. Question 5 of 9 For which of these sources is a null-aware spread the right choice? Choose every one.

    Choose every answer that is right.

    Show the answer: Question 5 of 9

    Answer:

    • A nullable Iterable<User> fetched from a database call
    • An optional Map<String, String>? extraParams argument
    • A List<int>? extra parameter that defaults to null

    Null-aware spread is for a nullable source whose contents, when it exists, should flow into the literal. Non-nullable sources need no '?', and a nullable single value needs a null-aware element.

  6. Question 6 of 9 final two = 2; const list = [1, two, 3]; What does the compiler say?

    Choose one answer.

    Show the answer: Question 6 of 9

    Answer: A compile-time error: 'Not a constant expression.'

    The compiler's words 'Not a constant expression.' are the probe output of this very code: 'two' is a final, and a final is not a constant expression.

  7. Question 7 of 9 Two parts of a program compute "final a = [1, 2, 3];" and "final b = [1, 2, 3];" independently. A unit test runs "expect(identical(a, b), isTrue);". The test fails. Which change makes it pass without changing what the lists hold?

    Choose one answer.

    Show the answer: Question 7 of 9

    Answer: Replace both with 'const [1, 2, 3]'

    The rule from the lesson is one line long: equal const literals share one object, equal plain literals do not. If identical has to be true, the right-hand sides have to be const.

  8. Question 8 of 9 final empty = []; — what is the static type of empty?

    Choose one answer.

    Show the answer: Question 8 of 9

    Answer: List<dynamic>

    Writing 'final empty = [];' loses every later type check on the list. 'final empty = <String>[];' is one character longer and keeps every later line safe.

  9. Question 9 of 9 infer_types.dart printed the runtimeType of "final bare = [];". Which type was it?

    Choose one answer.

    Show the answer: Question 9 of 9

    Answer: List<dynamic>

    The recorded run printed List<dynamic> for "final bare = []". Give the literal a target type or a type argument (<String>[]) when you want later code checked.

Interview questions

Warm-up (fresher): When should you reach for spread rather than add in a list literal?

Reach for spread when the extra items are already in a collection and should land in the result in order. [...head, ...tail] builds one list in one pass: the compiler sees the shape of the final list, inference picks the element type from the two sources, and no half-built list exists in between. add and addAll work on a list that is already there, so they ask you to allocate the outer list first, then change it, which means the result cannot be const, cannot be returned straight from an expression and cannot be the right-hand side of a => short-form function. If one of the sources might be null, use ...?maybe instead of a guard clause; it is one character, and the surrounding literal stays an expression you can read from top to bottom. Keep add for a loop that decides as it goes what joins the result, where no literal shape fits.

The module’s bank has two design questions that build on this section:

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Mid to senior

    A function buildQuery({String? q, int? page, List<String>? tags, Map<String, String>? extra}) returns a Map<String, String>. A teammate's version has four if statements and a mutable local map. Rewrite it as one literal, say what each feature does, and name the check the literal still cannot carry.

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A unit test asserts identical(buildPalette(), buildPalette()), where buildPalette returns a List<Color> of eight colours. The test fails on every run, although the two lists hold equal colours. A teammate suggests making the list final; a second suggests marking it const. Which is right, and what other changes do you have to make?

    Model answer, Pro. First step and follow-ups

Flashcards · 7 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What are collection if and collection for?

    Elements of a list, set or map literal, not statements: one literal can turn parts on and off, or repeat them, without a builder.

  2. Why does [if (signedIn) menu.add('Settings')] not compile?

    add changes a list in place and returns void, so the literal tries to insert a void. Write the element itself: if (signedIn) 'Settings'.

  3. What do spread, null-aware spread and null-aware elements do?

    ... inserts every value of a source; ...? treats a null source as empty; a null-aware element, since Dart 3.8, drops a null value or a null-valued map entry without a guard.

  4. Why is [...maybe] a compile error when maybe has a nullable list type?

    maybe could be null, and a plain spread needs a collection. ...?maybe treats null as empty and inserts nothing.

  5. What makes a const collection different from an ordinary one?

    It is canonical and deeply immutable: identical on two matching const literals is true, every item inside must be a constant, and the smallest write throws at run time.

  6. What types do a bare [] and a bare {} get with no context?

    List<dynamic> and Map<dynamic, dynamic>. Write the element type, such as <String>[] or <String>{}, when no context gives one.

  7. Does a literal with spreads reuse the collections it spreads?

    No. It always builds a new collection; the lazy Iterable methods walk several sources without building a list for the walk.

Key takeaways

  • Collection if and collection for are elements of a literal, so one list, set or map literal can turn parts on and off without a builder.
  • Spread inserts every value of a source; null-aware spread treats a null source as empty; null-aware elements, since Dart 3.8, drop a null value or a null-valued map entry without a guard.
  • A const collection is a canonical and deeply immutable value: identical on two matching const literals is true, every item inside must be a constant and the smallest write throws at run time.
  • A bare [] is a List<dynamic> and a bare {} is a Map<dynamic, dynamic>; write the element type (<String>[], <String>{}) when the context does not give one.
  • A literal with spreads always builds a new collection; the lazy Iterable API walks several sources without allocating a list for the walk.

Terms in this lesson

collection for
A for element inside a Dart list, set or map literal, with the shape for (var x in xs) e or a C-style for. It walks the loop and adds the element its body builds to the surrounding collection, zero or more times.
collection if
An if element inside a Dart list, set or map literal. It writes zero or one element into the surrounding collection and may carry an else branch, so one literal can turn a piece on or off from the data.
deep immutability
The guarantee that nothing inside a value ever changes, however many layers deep. A const collection in Dart is deeply immutable: its length, its elements and any const collections inside them all refuse change at run time.
null-aware element
A ? before an element or a map key or value, added in Dart 3.8. The element or entry is inserted only when its expression is not null, so a nullable value fits a non-nullable element type in one character.
null-aware spread
A ...? followed by a nullable collection, inside a Dart list, set or map literal. A null source is treated as empty: no element is inserted, and no guard is needed before the spread.
spread element
An ... followed by an Iterable, written inside a Dart list, set or map literal. It inserts every value of the source into the surrounding collection, in order, so one literal can build on another.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.