Working with Lists, Sets, and Maps
In the last chapter you learned how to write functions. Now we turn to collections — the containers Dart gives you for holding many values at once. Almost every real program stores groups of things: names in a list, unique tags in a set, settings in a map. This chapter walks through Dart's three core collections in depth: List, Set, and Map.
A collection is an object whose job is to hold other objects and let you work with them as a group.
The three containers at a glance
| Type | Holds | Ordering | Duplicates? | Fast lookup by |
|---|---|---|---|---|
| `List` | an ordered sequence | insertion order | yes | position (index) |
| `Set` | distinct values | usually insertion order | no | value |
| `Map` | key–value pairs | insertion order (usually) | unique keys only | key |
Choose based on the question you need to answer quickly:
- "Give me the third item" →
List. - "Have I seen this value before?" →
Set. - "What is the value for this key?" →
Map.
void main() {
var fruits = ['apple', 'banana', 'cherry']; // List
var unique = {'apple', 'banana', 'apple'}; // Set: 'apple' appears once
var ages = {'Ada': 36, 'Grace': 45}; // Map
print(fruits.length); // 3
print(unique.length); // 2
print(ages['Ada']); // 36
}Show diagram source
graph LR
subgraph List["List: ordered, indexed"]
L0["0: 'a'"] --> L1["1: 'b'"] --> L2["2: 'c'"] --> L3["3: 'd'"]
end
subgraph Set["Set: unique values"]
S1["'a'"] --- S2["'b'"] --- S3["'c'"] --- S4["'d'"]
end
subgraph Map["Map: key to value"]
K1["'Ada'"] --> V1["36"]
K2["'Grace'"] --> V2["45"]
endLists: your ordered workhorse
A List keeps values in order, and each value has a numbered position called an index, starting at 0. You create one with square brackets.
void main() {
var todo = ['wash dishes', 'write code'];
todo.add('walk dog'); // add at the end
todo.insert(0, 'make coffee'); // insert at a position
print(todo[0]); // make coffee
print(todo.last); // walk dog
print(todo.length); // 4
todo[1] = 'write Dart code'; // replace an item
todo.removeAt(0); // remove by position
todo.remove('walk dog'); // remove by value
print(todo); // [write Dart code, wash dishes]
}Dart lists are growable: they expand as you add items. If you want a fixed-length list, you can create one explicitly, but in everyday code growable lists are the norm.
A list can also be marked constant with final or const. The difference matters:
final list = [...]— you cannot reassign the variable, but you can still change the list's contents.const list = [...]— the list itself is frozen; adding or removing raises an error.
Sets: when every value must be unique
A Set stores each value at most once. Adding a duplicate does nothing. This makes sets perfect for questions like "which tags did this article get?" or "who has already signed up?"
void main() {
var seen = <String>{};
for (var name in ['Ada', 'Alan', 'Ada', 'Grace']) {
if (seen.add(name)) { // add returns true if the value was new
print('first time seeing $name');
} else {
print('already saw $name');
}
}
print(seen.contains('Alan')); // true
print(seen.length); // 3
}Sets also support math-like operations that answer comparison questions directly:
void main() {
var alice = {'dart', 'python', 'sql'};
var bob = {'dart', 'rust'};
print(alice.intersection(bob)); // {dart} — both know
print(alice.union(bob)); // {dart, python, sql, rust} — either knows
print(alice.difference(bob)); // {python, sql} — only alice knows
}Maps: key–value lookups
A Map associates each key with exactly one value. Keys are unique; values can repeat. Think of a real dictionary: you look up a word (key) and get its definition (value).
void main() {
var stock = {'apple': 5, 'banana': 0};
stock['cherry'] = 12; // add a new pair
stock['apple'] = 4; // update an existing value
stock.remove('banana');
print(stock.containsKey('apple')); // true
print(stock.keys); // (apple, cherry)
print(stock.values); // (4, 12)
}The missing-key trap
Here is the single most common Map surprise for newcomers. If you ask for a key that does not exist, Dart returns null rather than an error:
void main() {
var stock = {'apple': 5};
int count = stock['pear']; // ERROR at runtime: null is not an int
}With Dart's null safety (covered earlier in this tutorial), a plain int cannot be null, so this line fails. There are three safe fixes:
void main() {
var stock = {'apple': 5};
// 1. Ask for a nullable type and handle null yourself.
int? maybe = stock['pear'];
print(maybe ?? 0);
// 2. Provide a default right in the lookup.
int count = stock['pear'] ?? 0;
print(count); // 0
// 3. Use putIfAbsent to insert a default if the key is missing.
stock.putIfAbsent('pear', () => 0);
print(stock); // {apple: 5, pear: 0}
}The ?? operator ("if null") is your friend here: it returns the left value unless it is null, in which case it returns the right value.
Looping over collections
All three collections can be walked with a for-in loop:
void main() {
var fruits = ['apple', 'banana'];
for (var fruit in fruits) {
print(fruit);
}
var ages = {'Ada': 36, 'Grace': 45};
ages.forEach((name, age) => print('$name is $age'));
// Need the position too? Use an indexed loop.
for (var i = 0; i < fruits.length; i++) {
print('$i: ${fruits[i]}');
}
}Note what is not in these loops: you should not add or remove items from a list while looping over it. Dart may throw an error, and even when it does not, the results are confusing. Collect the changes and apply them after the loop.
Spreading and collecting
Two pieces of syntax make building collections easier. The spread operator ... inserts all the elements of one collection into another:
void main() {
var base = ['bread', 'milk'];
var extras = ['eggs', 'cheese'];
var cart = [...base, ...extras, 'jam'];
print(cart); // [bread, milk, eggs, cheese, jam]
// The null-aware spread ...? inserts nothing if the source is null.
List<String>? coupons;
var checkout = [...cart, ...?coupons];
print(checkout.length);
}There is also a collection if and collection for, which let you build lists conditionally or from another collection:
void main() {
var fancy = true;
var order = ['coffee', if (fancy) 'whipped cream', for (var i in [1, 2]) 'shot $i'];
print(order); // [coffee, whipped cream, shot 1, shot 2]
}Choosing well: performance in plain terms
For an everyday mental model:
- List indexing by position is instant, but checking whether a list contains a value scans the whole list.
- Set and Map lookups check a value or key almost instantly, no matter how many items they hold.
- If you find yourself calling
list.contains(x)inside a loop, that loop is usually much faster with a Set instead.
Exercises
Exercise
You have a list of visitor names with duplicates: ['Ana', 'Bo', 'Ana', 'Cy', 'Bo']. How do you get a count of how many distinct people visited?
Show answer
Convert the list to a set and read its length: names.toSet().length gives 3. The set drops the duplicates, so its length counts distinct visitors.
Exercise
Write a lookup that safely reads prices['tea'] from a map and prints 0 if the key is missing.
Show answer
print(prices['tea'] ?? 0); — the ?? operator substitutes 0 when the map returns null for a missing key.
Exercise
You need a collection that answers "what is the email address for user ID 42?" as fast as possible. List, Set, or Map?
Show answer
A Map, keyed by the user ID. Maps give near-instant lookup by key, while a List would require scanning until you found the right entry.
Spotted a mistake, or code that does not work?