← Back to blog
Dart Fundamentals · Part 6 of 10
July 4, 202610 min read

Collections in Dart: List, Set & Map (and How They Differ)

DartFlutter

Collections in Dart

This is Part 6 of the Dart Fundamentals series. We've covered types (Part 4) and null safety (Part 5) — now we get to the data structures you'll use in basically every program: List, Set, and Map.

The good news: Dart's collection literals are some of the nicest in any language, thanks to spreads and "collection-if/for." By the end of this post you'll know which collection to reach for and how to build them elegantly.


The three collections at a glance

| Collection | Keeps order? | Allows duplicates? | Looked up by | Literal | | ---------- | ------------ | ------------------ | --------------- | ----------- | | List | ✅ Yes | ✅ Yes | index (0,1,2) | [a, b, c] | | Set | ❌ No* | ❌ No (unique) | value | {a, b, c} | | Map | ✅ Insertion | keys unique | key | {k: v} |

*Dart's default Set actually preserves insertion order, but conceptually you should treat a set as unordered.

Pick by the question you're answering:

  • "What's the 3rd item?"List (ordered, indexed).
  • "Have I seen this before / give me only unique values"Set.
  • "What's the value for this key?"Map.

Let's take them one at a time.


List — ordered and indexed

A List is an ordered group of items you access by position. This is the workhorse:

var fruits = ['apple', 'banana', 'cherry'];

print(fruits[0]);      // apple   (indexes start at 0)
print(fruits.length);  // 3
print(fruits.first);   // apple
print(fruits.last);    // cherry

Add, insert, and remove:

fruits.add('date');          // → [apple, banana, cherry, date]
fruits.insert(0, 'avocado'); // → [avocado, apple, banana, cherry, date]
fruits.remove('banana');     // removes by value
fruits.removeAt(0);          // removes by index

Typed lists and inference

From Part 4, remember that an empty literal can't infer its element type — annotate it:

var scores = <int>[];   // List<int>, not List<dynamic>
scores.add(10);
// scores.add('x');      // ❌ caught at compile time

The everyday transforming methods

You'll lean on these constantly — they each return a new iterable, leaving the original alone:

var nums = [1, 2, 3, 4, 5];

nums.map((n) => n * 2);             // (2, 4, 6, 8, 10)
nums.where((n) => n.isEven);        // (2, 4)
nums.firstWhere((n) => n > 3);      // 4
nums.any((n) => n > 4);             // true
nums.every((n) => n > 0);           // true
nums.fold(0, (sum, n) => sum + n);  // 15
nums.reduce((a, b) => a + b);       // 15

(map/where return a lazy Iterable; call .toList() when you need a concrete List.)


Set — unique values, fast membership

A Set holds unique values and is built for one question above all: "is this in here?" Checking membership in a set is far faster than scanning a list.

var visited = <String>{'home', 'about', 'home'};
print(visited); // {home, about} — the duplicate 'home' is dropped

Notice duplicates are silently ignored — that's the defining feature.

visited.add('contact'); // {home, about, contact}
visited.add('home');    // no effect — already present

print(visited.contains('about')); // true — and this is FAST

⚠️ The empty-set gotcha (read this twice)

Both sets and maps use curly braces. So what is {}? Dart resolves the ambiguity in favour of Map:

var a = {};        // ❌ this is a Map<dynamic, dynamic>, NOT a set!
var b = <String>{}; // ✅ THIS is an empty Set<String>

If you want an empty set, you must give it a type: <String>{}. Forget the type annotation and you'll get a map, which leads to baffling errors. This bites everyone once.

Set algebra

Sets give you the classic mathematical operations — genuinely handy:

var a = {1, 2, 3, 4};
var b = {3, 4, 5, 6};

a.union(b);        // {1, 2, 3, 4, 5, 6} — everything
a.intersection(b); // {3, 4}             — in both
a.difference(b);   // {1, 2}             — in a but not b

Deduplicating a list is a one-liner with this:

var withDupes = [1, 2, 2, 3, 3, 3];
var unique = withDupes.toSet().toList(); // [1, 2, 3]

Map — key/value pairs

A Map associates keys with values — a dictionary/lookup table. Keys are unique; values can repeat.

var ages = {
  'Asha': 30,
  'Ben': 25,
  'Cara': 30, // value can repeat...
};

print(ages['Asha']);  // 30
print(ages['Nobody']); // null — missing key returns null, never crashes

Note that returned type: looking up a key gives a nullable value (int? here), because the key might not exist. That's null safety from Part 5 protecting you again.

int age = ages['Asha'] ?? 0; // supply a default for the maybe-missing key

Add, update, check, remove:

ages['Dan'] = 40;            // add or overwrite
ages.containsKey('Ben');     // true
ages.remove('Ben');          // removes the entry
ages.putIfAbsent('Eve', () => 22); // add only if missing

Iterating a map:

ages.forEach((name, age) => print('$name is $age'));

for (final entry in ages.entries) {
  print('${entry.key} → ${entry.value}');
}

ages.keys;   // the names
ages.values; // the ages

The fun part: building collections elegantly

This is where Dart pulls ahead of most languages. Three features turn clunky build-up code into clean, declarative literals.

Spread operator ... — pour one collection into another

var base = [1, 2, 3];
var more = [0, ...base, 4]; // [0, 1, 2, 3, 4]

No addAll loops — just splat the elements right into the new literal. Works for sets and maps too:

var defaults = {'theme': 'light', 'lang': 'en'};
var settings = {...defaults, 'theme': 'dark'}; // later key wins → theme is 'dark'

Null-aware spread ...? — spread only if not null

If the thing you're spreading might be null, the plain ... would crash. ...? skips it gracefully:

List<int>? extra = maybeGetItems(); // could be null
var all = [1, 2, ...?extra];        // if extra is null, just [1, 2]

Collection-if — conditionally include an element

Add an item only when a condition holds, right inside the literal:

bool isAdmin = true;

var menu = [
  'Home',
  'Profile',
  if (isAdmin) 'Admin Panel', // included only when isAdmin is true
];
// → [Home, Profile, Admin Panel]

Flutter developers live in this one — conditionally adding widgets to a list without breaking the declarative flow.

Collection-for — build elements with a loop, inline

Generate elements from another iterable, right inside the brackets:

var nums = [1, 2, 3];
var doubled = [for (final n in nums) n * 2]; // [2, 4, 6]

And you can combine collection-for with collection-if for a filter-and-transform in one breath:

var nums = [1, 2, 3, 4, 5, 6];
var evenSquares = [
  for (final n in nums)
    if (n.isEven) n * n,
]; // [4, 16, 36]

Read that almost like English: "for each n, if it's even, include its square." That's the elegance Dart's collection literals are known for.


const and unmodifiable collections

You can make a collection a compile-time constant (recall const from Part 2). A const collection is deeply immutable — you can't add, remove, or change anything:

const days = ['Mon', 'Tue', 'Wed'];
// days.add('Thu'); // ❌ Unsupported — a const list is frozen

A neat detail: the const can sit on the value even when the variable isn't const, which is great for fixed lookup tables you don't want mutated:

final colors = const ['red', 'green', 'blue']; // immutable contents, reassignable variable

Practice Challenges

Give each a real attempt first.

Challenge 1 — Which collection? For each task, name the best collection: (a) the order of pages a user visited, (b) the set of unique tags on a post, (c) a phone book mapping names to numbers.

Show solution

(a) List — order matters and pages can repeat. (b) Set — unique, membership matters, order doesn't. (c) Map — name (key) → number (value) lookups.

Challenge 2 — Deduplicate while preserving the idea. Given var nums = [3, 1, 3, 2, 1];, produce a list of the unique values.

Show solution
var unique = nums.toSet().toList(); // [3, 1, 2]

.toSet() drops duplicates; .toList() turns it back into a list. (Default Set keeps insertion order, so you get [3, 1, 2].)

Challenge 3 — Spot the bug. A developer wants an empty set of ints but gets weird type errors. What's wrong?

var ids = {};
ids.add(1);
Show solution

{} is an empty Map, not a set — so ids.add(1) doesn't even exist on it. Fix by annotating the type:

var ids = <int>{}; // now it's a Set<int>
ids.add(1);

Challenge 4 — One-line filtered build. From [1..10], build a list of the squares of the odd numbers using collection-for and collection-if.

Show solution
var nums = [for (var i = 1; i <= 10; i++) i];
var oddSquares = [
  for (final n in nums)
    if (n.isOdd) n * n,
];
// → [1, 9, 25, 49, 81]

Challenge 5 — Merge with overrides. Given defaults = {'size': 'M', 'color': 'red'} and userPrefs = {'color': 'blue'}, produce a merged map where the user's choices win.

Show solution
var merged = {...defaults, ...userPrefs};
// → {size: M, color: blue}  — userPrefs spread last, so its color wins

When the same key appears twice in a map literal, the later one takes effect — so spread the overrides last.

Challenge 6 — Safe spread. List<String>? tags might be null. Build a list ['post', ...tags?]-style without crashing when it's null.

Show solution
var all = ['post', ...?tags]; // null-aware spread skips it if tags is null

...? is the null-aware spread — if tags is null, nothing is added instead of throwing.


Check Yourself (Q&A)

Q1. List vs Set — when do I pick Set? When you need unique values and/or fast "does it contain X?" checks, and order isn't the point. Lists keep order and allow duplicates but membership checks scan the whole thing.

Q2. What does {} create? An empty Map, not a Set. For an empty set you must annotate: <String>{}.

Q3. Why is a Map lookup nullable? Because the key might not exist — map[key] returns null for a missing key rather than crashing. Use ?? default to handle the miss.

Q4. What's the difference between ... and ...?? ... spreads a collection's elements into a literal; ...? does the same but safely skips the operand when it's null.

Q5. Are map/where results lists? No — they return lazy Iterables. Call .toList() (or .toSet()) to materialize them when you need a concrete collection.

Q6. Can I mutate a const list? No. const collections are deeply immutable — add/remove/update all throw. Use a normal final list if you need to mutate the contents.


Wrapping Up

  • List = ordered + indexed + duplicates allowed → [...].
  • Set = unique values + fast membership → <T>{...} (and remember {} is a Map!).
  • Map = key→value lookups, missing keys return null → {k: v}.
  • Build them beautifully with ... (spread), ...? (null-aware spread), collection-if, and collection-for — often combined into a single declarative literal.
  • const collections are deeply frozen.

Next, in Part 7, we tackle functions: positional, named, and optional parameters, the arrow => shorthand, closures, and returning multiple values with records.