Course outline

Dart / Foundations: values, absence, collections, and valid objects / Lesson 4

Choose collections and understand lazy Iterable work

Use typed lists, sets, maps, collection elements, lazy transformations, and explicit snapshots without confusing interfaces with immutability.

About 5 min readContent reviewed
On this page

Lesson goals

  • Choose ordering, uniqueness, and lookup semantics deliberately.
  • Explain when map and where evaluate and what an unmodifiable copy actually protects.

The collection shape is part of the data contract

List preserves an indexed order and allows repeated values. Set models unique elements under its equality contract. Map associates keys with values and returns a nullable result from ordinary lookup because a key can be absent. An empty {} literal is a map unless type context says otherwise; an explicit <String>{} clearly declares a set. Choose a collection from meaning rather than punctuation. Repeated sample labels may be accidental duplicates or separate observations, so converting to a set can lose legitimate data. Key replacement is also different from grouping several values under one key.

Dart Iterable transformations can defer work

For an ordinary Iterable, map and where return lazy views of a computation rather than eagerly completed lists. A terminal consumer such as toList requests elements and performs the work. Repeated iteration can repeat callbacks and observe changes in an underlying mutable source, so avoid putting important side effects into a transformation. The example counts callback calls to expose timing, not to recommend mutation inside map. A developer arriving from a language with eager list transformations should check Dart’s actual contract instead of assuming the same method name means the same evaluation strategy.

dart Observe lazy map evaluation
void main() {
  var visited = 0;
  final mapped = <int>[1, 2, 3].map((value) {
    visited += 1;
    return value * 2;
  });
  print(visited);
  print(mapped.toList());
  print(visited);
}
Expected output
0
[2, 4, 6]
3

Expected output is 0, [2, 4, 6], then 3. Creating mapped does not invoke the callback; toList iterates all three elements. The source was not executed.

Build results without hiding mutation in traversal

Collection literals can include spread elements, conditional if elements, and for elements. These forms can construct a result directly while keeping selection and ordering visible. A spread copies the iterated element references into the new container; it does not deeply clone those elements. A null-aware spread handles a nullable source according to its operator contract, not malformed elements inside the source. Keep expressions readable and avoid deeply nested collection control flow when a named helper would explain policy more clearly. A shorter literal is useful only if its data meaning remains understandable.

An unmodifiable list still has shallow element semantics

Dart’s List interface includes mutation operations; a List type alone is not a read-only guarantee. An unmodifiable list rejects container mutation, while a view and a copied snapshot can have different relationships to source changes. List.unmodifiable creates an unmodifiable list containing the supplied element references. Mutable element objects may still change internally. The example uses strings to isolate container ownership: later source additions do not change the snapshot. Choose the specific API whose copy or view semantics match the promise, and do not describe a shallow container copy as deep immutability.

dart A shared alias, a snapshot, and map replacement
void main() {
  final source = <String>['sample-A'];
  final alias = source;
  final snapshot = List<String>.unmodifiable(source);
  source.add('sample-B');
  print(alias.length);
  print(snapshot.length);
  final counts = <String, int>{'A': 1};
  counts['A'] = 2;
  print(counts['A']);
  print(counts['missing'] ?? -1);
}
Expected output
2
1
2
-1

Expected output is 2, 1, 2, -1. Updating key A replaces its value; absent lookup chooses the fallback. No mutation of snapshot is attempted, and the program was not run.

Keep result size and empty behavior explicit

A lazy pipeline can still consume unbounded work when its terminal operation requests everything. Sorting and materializing large results require memory even when earlier stages were lazy. Operations that expect an element, such as first, need an empty-input policy; do not invent an identifier to conceal absence. When a map permits nullable values, containsKey distinguishes a present null from a missing entry. Review collection equality separately from element equality and keep hash keys stable. These choices should follow the application’s data contract rather than a blanket rule that laziness or unmodifiable containers always solve correctness.

Common errors and fixes

A map callback appears not to run.

Why it happens
The returned Iterable was never consumed.
How to fix it
Consume it deliberately or perform an explicit eager operation when required.
Prevent it next time
Keep side effects out of lazy transformations so timing is not surprising.

An unmodifiable list contains objects that still change.

Why it happens
Container protection was mistaken for deep element immutability.
How to fix it
Choose immutable elements or an explicit deeper copy/ownership policy.
Prevent it next time
Inspect nested mutable state before promising a stable snapshot.

Repeated observations vanish after conversion to a set.

Why it happens
Uniqueness was imposed without checking whether repetition had meaning.
How to fix it
Preserve the list or group observations according to the domain.
Prevent it next time
Define duplicate handling at the input boundary.

Check your understanding

1. Will iterating a lazy mapped Iterable twice always reuse one stored result list?

Show worked solution

No. A typical lazy mapped Iterable can invoke the transformation again for each traversal, and changes to its source can matter. Materialize a deliberate snapshot when reuse and stability are required, then consider shallow element references. The API contract, not the presence of final on the variable, determines whether computation or data is stored.

2. Why is List.unmodifiable not enough to make a list of mutable model objects deeply immutable?

Show worked solution

It prevents changes to the list container but retains references to the element objects. Another owner can still mutate those objects and change what readers observe. Use value-like immutable elements, carefully defined copying, or exclusive ownership where required. Verify the actual nested state instead of inferring a stronger guarantee from the constructor name.

Key takeaways

  • Dart Iterable laziness changes when work and side effects occur.
  • Ordering, uniqueness, snapshots, and element ownership are separate contracts.

Official references