Ankit Ranjan
Back to Deep Dives

Collections in Dart — Lists, Sets, Maps, and Performance Trade-offs

Three collections, three problems. How Dart organises bulk data — from phone contacts to high-performance arrays — and how to pick the right tool for the job.

May 22, 2026 7 topics 7 quiz questions
Share:
1

The contacts problem — three answers

Open the contacts app on your phone. You see a long list of contacts. How does Dart actually store all of that?

There are several built-in data types that can hold bulk data. Each one solves a slightly different problem, and choosing the right one shapes the performance of the whole program.

Let's start with the same set of phone numbers and see what each collection does with them.

Three answers to the contacts problemListordered · duplicates allowed[0]987654321[1]123456789[2]9876102345[3]7876413232[4]123456789duplicate at [4]Setunique items · insertion-ordered98765432112345678998761023457876413232(duplicate dropped)4 entries, 5 inputsMapkey → value · lookup by nameAnkit → 987654321Mom → 123456789Dad → 9876102345Brother→ 7876413232Sister → 123456789same number, different name — OKSame data, three structures. Each one loses something the others keep.

List keeps the order we put things in. It also keeps the duplicate at [4] — Lists don't ask any questions. Set silently drops the duplicate, but loses the position index. Map attaches each number to a name, which is what we usually want for contacts — but now the structure is fundamentally different.

So how do we pick? The mental algorithm is small enough to memorise.

Which collection should I use?Need to look upby a key (like a name)?yesMapname → numbernoNeed orderedaccess by position?(or duplicates?)yesListordered, indexednoSetunique items, fast lookupWhen all three would work, prefer the most restrictive — it documents intent.

With the map in our head, let's go deep on each one.

2

Lists — ordered and growable

A List is an ordered collection of objects. Each element has an index, starting at zero. We can store anything in a list, including duplicates.

void main() {
  List phoneNumbers = [
    '987654321',
    '123456789',
    '9876102345',
    '7876413232',
    '123456789',   // happily kept — Lists ask no questions
  ];
  print(phoneNumbers);
  print(phoneNumbers.length);   // 5
  print(phoneNumbers[2]);       // '9876102345'
}
Common operations on a list include:

phoneNumbers.add('555444333');         // append at end
phoneNumbers.insert(0, '111000222');    // insert at front
phoneNumbers.removeAt(1);               // remove by position
phoneNumbers.remove('123456789');       // remove first match by value
phoneNumbers.contains('987654321');     // true
phoneNumbers.indexOf('9876102345');     // 2
phoneNumbers.sort();                    // in-place sort
List flavours. Not all lists are equal. Dart gives us four shapes of list, each with its own rules.

List flavors — same data, different rulesPropertygrowable[1, 2, 3]fixed-lengthList.filled(...)unmodifiableList.unmodif...constconst [1,2,3]Can add / remove?Can replace at [i]?Built at compile time?Best for…most everydayuse casesknown size,tight memorydefensive copies,public API safetytruly static data(canonicalised)

How adding to a growable list actually works. When we write list.add(x), Dart appends to a backing array. But what happens when that array runs out of room?

How a growable List actually grows1. Start: length 5, capacity 8ABCDEfilledspare capacity2. After 3 more adds: length 8, capacity 8 — now fullABCDEFGHadd('I') →allocate 2× array,copy contents3. After add: length 9, capacity 16 — doubledABCDEFGHIAmortized O(1) — most adds are cheap, occasional doubling pays the rare cost.

Most calls to add are cheap — they just write to an empty slot. Once in a while, the backing array fills up and Dart has to allocate a new array twice the size and copy everything over. That copy is expensive, but it happens rarely enough that the average cost stays constant. This is what "amortized O(1)" means.

3

Sets — when uniqueness matters

Lists happily store the same number twice. That's a feature for sequences (a chat log can repeat itself), but for a phonebook it's a bug. We want to drop the duplicate entry.

That's what a Set does. It keeps every distinct element exactly once.

void main() {
  var phoneNumbers = [
    '987654321', '123456789', '9876102345', '7876413232', '123456789',
  ];

  Set uniqueNumbers = {...phoneNumbers};   // spread into a Set
  print(uniqueNumbers);          // {987654321, 123456789, 9876102345, 7876413232}
  print(uniqueNumbers.length);   // 4 — the duplicate is gone
}
A surprise about Dart's default Set. When we write {...} or call Set(), we get a LinkedHashSet under the hood. The "linked" part means Dart secretly tracks insertion order, so iterating over the set produces elements in the order we put them in. Most other languages don't promise this; Dart does.

Set operations. The classic three from set theory — union, intersection, difference — are built into Dart's Set, and a Venn diagram is the fastest way to see what each one does.

Set operations — A = {1,2,3,4}, B = {3,4,5,6}uniona.union(b)AB123456{1, 2, 3, 4, 5, 6}all elements from eitherintersectiona.intersection(b)AB123456{3, 4}elements in bothdifferencea.difference(b)AB123456{1, 2}in A but not in BDart's Set provides .union(), .intersection(), and .difference() out of the box.

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

print(a.union(b));         // {1, 2, 3, 4, 5, 6}
print(a.intersection(b));  // {3, 4}
print(a.difference(b));    // {1, 2}
The performance jewel — Set's contains is fast. Checking whether a List contains a value means scanning every element until we find it, which is O(n). Checking whether a Set contains a value uses hashing and runs in O(1) on average. For large collections, this is a thousand-times-faster kind of difference. We'll see exactly how Set pulls that off in the next topic, because Map uses the same trick.

4

Maps — the phonebook completed

A Set deduplicates phone numbers, but we've still lost the name. To go from "9876…" back to "Mom" we need the key→value relationship that a Map provides.

void main() {
  Map phoneNumbers = {
    'Ankit':   '987654321',
    'Mom':     '123456789',
    'Dad':     '9876102345',
    'Brother': '7876413232',
    'Sister':  '123456789',
  };
  print(phoneNumbers);
  print(phoneNumbers['Mom']);   // '123456789'
}
Notice that Sister and Mom can share the same number — values don't have to be unique, only keys do. That matches real life: two people can have the same phone, but they have different names.

The empty-literal gotcha. Dart's collection literal syntax has one trap that catches almost everyone the first time.

What does {} mean? It depends on the context.{}Map<dynamic, dynamic>empty Map,NOT empty Set<int>{}Set<int>explicit type tellsDart it is a Set{1, 2, 3}Set<int>commas → Set(no colons){1: 'a'}Map<int, String>key:value pairs→ MapRule: bare {} defaults to Map. For an empty Set, write <Type>{} or Set<Type>().

The bare {} looks like an empty set, but it's actually an empty Map. To get an empty Set, we need to give Dart a type hint or use the explicit constructor.

How Map (and Set) achieve O(1) lookup. When we write phoneNumbers['Mom'], Dart doesn't scan the whole map. It runs the key through a hash function, which gives it an integer. That integer, modulo the bucket count, tells Dart exactly which bucket to look in. Inside the bucket, it compares keys to find the match.

How Set and Map look up in O(1) — hash bucketsstep 1: the key"Mom"hash()3729183617% 83→ bucket indexstep 2: the bucket array[0]empty[1]empty[2]Ankit→ 987...[3]Mom→ 123...Dad→ 987...[4]empty[5]Sister→ 555...[6]empty[7]step 3: scan the small bucket for matching key"Mom" and "Dad" both hashed to bucket [3] — a collision.The bucket stores both as a small list; we compare keys to find the right one.Buckets stay short → average lookup is O(1), regardless of total size.

The whole point of hashing is to skip the linear scan. We compute the bucket index once, then look only inside that single bucket. Sets work the same way — they're basically Maps where we only care about the keys.

Three Map flavours. Just like List, Map comes in a few shapes that pick different trade-offs.

Three Map flavors — same API, different rulesPropertyLinkedHashMap{} or Map() — the defaultHashMapHashMap()SplayTreeMapSplayTreeMap()Iteration orderinsertion orderunspecifiedsorted by keyLookup speedO(1) averageO(1) averageO(log n)Use when…most cases — fastAND predictableiterationorder truly doesn'tmatter; slightlylower overheadneed keys sorted(e.g., range queries,ordered traversal)

Common Map operations to know:

phoneNumbers['Cousin'] = '555000111';   // add or update
phoneNumbers.containsKey('Mom');         // true
phoneNumbers.containsValue('123456789'); // true (slower — O(n))
phoneNumbers.remove('Brother');
phoneNumbers.keys;       // Iterable of keys
phoneNumbers.values;     // Iterable of values
phoneNumbers.entries;    // Iterable of MapEntry(key, value)

5

Iteration and transformation

Once we have a collection, the next thing we want to do is walk through it. Dart has several ways, ranging from the imperative for loop to functional one-liners.

The basic loops.

// Classic for-in
for (var number in phoneNumbers) {
  print(number);
}

// For a Map, iterate keys, values, or both
for (var name in phoneBook.keys) { ... }
for (var number in phoneBook.values) { ... }
for (var entry in phoneBook.entries) {
  print('${entry.key} → ${entry.value}');
}

// forEach takes a function
phoneNumbers.forEach((n) => print(n));
The functional toolkit. Most real iteration code reaches for map, where, and fold instead of explicit loops. They're shorter, clearer, and chainable.

var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

// map — transform each element
var doubled = numbers.map((n) => n * 2);
// (2, 4, 6, 8, 10, 12, 14, 16, 18, 20)

// where — filter
var evens = numbers.where((n) => n.isEven);
// (2, 4, 6, 8, 10)

// fold — collapse to a single value
var sum = numbers.fold(0, (acc, n) => acc + n);
// 55

// chain them
var sumOfDoubledEvens = numbers
  .where((n) => n.isEven)
  .map((n) => n * 2)
  .fold(0, (acc, n) => acc + n);
// 60
The Iterable hierarchy. List, Set, and the keys/values of a Map are all Iterable. So is the result of where or map. This is why we can chain operations across different collection types — they all speak the same iteration protocol.

The lazy evaluation gotcha. Here's the surprise that bites people. where and map return lazy Iterables. Nothing actually happens until we iterate. And every time we iterate, the work runs again.

var numbers = [1, 2, 3, 4, 5];
var bigOnes = numbers.where((n) {
  print('checking $n');
  return n > 3;
});

// Nothing has been printed yet!

print(bigOnes.first);   // prints "checking 1" through "checking 4", then 4
print(bigOnes.length);  // prints "checking 1" through "checking 5" AGAIN — re-runs the filter
For a lazy iterable used many times, the filter runs every time. The fix is to materialise with .toList() or .toSet() when we'll use the result more than once:

var bigOnes = numbers.where((n) => n > 3).toList();
// now bigOnes is a real List, work happens once
Reach for lazy iterables when chaining transformations (the laziness avoids building intermediate lists). Reach for .toList() when storing or reusing the result.

6

Performance — picking the right collection

Now that we know what each collection can do, the real question is when to pick which. The answer almost always comes down to the access pattern.

Big-O cheat sheet — pick by access patternOperationListSetMapAccess by indexlist[i]O(1)— not applicable— n/aAdd at endadd(x)O(1) amortizedO(1) averageO(1) avgm[k] = vInsert at start / middleinsert(i, x)O(n)— unordered— n/aRemove by valueremove(x)O(n)O(1) averageO(1) avgremove(k)Check membershipcontains(x)O(n)O(1) averageO(1) avgcontainsKeyLookup by keym[k]— no keys— use containsO(1) averageIterate every elementfor (var x in c)O(n)O(n)O(n)fast (constant time)O(log n) — acceptableO(n) — gets slow at scalenot applicableDefault Set/Map are LinkedHashSet/LinkedHashMap. Averages assume a healthy hash distribution.

The big lessons from the table:

• If we'll ever call contains on a big collection, use a Set. The List version is O(n) — a thousand-element scan for every check.
• If we want to look up by name, ID, or any other "key," use a Map. Don't simulate it with a List of records.
• If we need positional access (the third item, the last item, the items in order), use a List.
Inserting at the start of a List is O(n) — every other element has to shift. If we're doing this a lot, our data structure is probably wrong.

The TypedData escape hatch for numeric data. Everything in this table is the cost for general-purpose collections that store any kind of object. For large numeric arrays, we have a different option — the typed lists from Episode 3 (Uint8List, Int32List, Float64List) store unboxed values contiguously and skip the per-element pointer overhead. For image, audio, or sensor data, that's the right choice. The boxing story we covered in Episodes 3 and 4.5 applies in full here: a List<int> of byte-range values can use roughly 8 times the memory of the equivalent Uint8List. When numbers are the data, TypedData wins.

7

Power features — spread, collection-if, collection-for

Dart has a few syntax features that make working with collections genuinely enjoyable. They blur the line between defining a collection and computing one.

The spread operator. Three dots flatten a collection into another collection literal.

void main() {
  List<int> list1 = [1, 2, 3];
  List<int> list2 = [4, 5, 6];

  // Combine two lists
  var combined = [...list1, ...list2];   // [1, 2, 3, 4, 5, 6]

  // Copy and extend
  var extended = [...list1, 7, 8, 9];    // [1, 2, 3, 7, 8, 9]

  // Works for Set and Map literals too
  var uniqueNumbers = {...phoneList};
  var merged = {...mapA, ...mapB};
}
Spread also has a null-aware version ...? that's a no-op when the source is null — handy when building collections from optional inputs.

Collection-if. Inline conditionals inside collection literals. The first time we see this, it feels like magic.

bool isLoggedIn = true;
bool hasNotifications = false;

var menuItems = [
  'Home',
  'About',
  if (isLoggedIn) 'Profile',
  if (isLoggedIn) 'Settings',
  if (hasNotifications) 'Inbox',
  'Logout',
];
// ['Home', 'About', 'Profile', 'Settings', 'Logout']
Compare that to the imperative alternative — three separate if blocks each calling add — and the collection-if version is harder to get wrong.

Collection-for. The same idea, but with a loop.

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

var doubled = [for (var n in numbers) n * 2];
// [2, 4, 6, 8, 10]

var phoneTags = [
  for (var entry in phoneBook.entries)
    '${entry.key}: ${entry.value}',
];

// Combine collection-if and collection-for
var bigNumbers = [
  for (var n in numbers) if (n > 2) n * 10,
];
// [30, 40, 50]
Const collections. When a collection's contents are known at compile time and never change, prefix it with const. Dart will allocate it exactly once and reuse the same canonical instance — much like the canonical booleans from Episode 2 and string literals from Episode 5.

const validRoles = {'admin', 'editor', 'viewer'};
const httpCodes = {200: 'OK', 404: 'Not Found', 500: 'Error'};
Unmodifiable views. Wrap a collection to expose it without letting callers mutate it. Great for public API surfaces where we want to share data but keep ownership.

final _internalList = [1, 2, 3];
List<int> get items => List.unmodifiable(_internalList);

var view = items;
view.add(4);   // throws — UnsupportedError


That completes the data-foundation arc of this series. Booleans for transistor states. Integers and doubles for counting and measuring. Strings for text. Collections for the bulk data of every real program. Maps especially are the workhorse of app development — we'll see them everywhere when we move on to Flutter, from widget configuration to API responses to state management. Now we have the right mental model for picking the right one.

Test your understanding

7 questions

Seven questions covering the three core collections, their performance characteristics, and Dart's collection literals.

Search

Loading search...