Collections: Maps & Sets
Introduction to Maps and Sets
In the previous lesson we explored Lists, Dart’s ordered collection. Now we turn to two equally important collection types: Maps and Sets. Maps store data as key-value pairs, perfect for lookups and dictionaries. Sets store unique, unordered values, ideal for membership tests and eliminating duplicates. Together with Lists, these three types cover virtually every data-structure need in Dart.
Maps: Key-Value Pairs
A Map associates keys with values. Each key is unique and maps to exactly one value. Think of it like a real dictionary: you look up a word (the key) and get its definition (the value).
Creating Maps
There are several ways to create a Map in Dart:
Map Literal Syntax
void main() {
// Type-annotated map literal
Map<String, int> ages = {
'Ahmed': 25,
'Sara': 30,
'Ali': 22,
};
// Type-inferred map literal
var capitals = {
'Saudi Arabia': 'Riyadh',
'Egypt': 'Cairo',
'Jordan': 'Amman',
};
// Empty map with explicit types
Map<String, double> prices = {};
// Empty map using constructor
var scores = Map<String, int>();
print(ages); // {Ahmed: 25, Sara: 30, Ali: 22}
print(capitals); // {Saudi Arabia: Riyadh, Egypt: Cairo, Jordan: Amman}
}
var emptyMap = {};, Dart infers the type as Map<dynamic, dynamic>. Always specify the type explicitly for empty maps: Map<String, int> emptyMap = {}; or var emptyMap = <String, int>{};.Map<K, V> Type Syntax
The generic type Map<K, V> specifies the key type K and value type V. This ensures type safety at compile time.
Typed Maps
Map<String, int> wordCount = {}; // String keys, int values
Map<int, String> idToName = {}; // int keys, String values
Map<String, List<String>> groups = {}; // String keys, List values
Map<String, Map<String, int>> nested = {}; // Nested maps
Accessing and Modifying Map Entries
You use bracket notation to read, add, update, and remove entries in a map:
Accessing Map Values
void main() {
var user = {
'name': 'Edrees',
'email': 'edrees@example.com',
'age': '28',
};
// Reading a value
print(user['name']); // Edrees
print(user['email']); // edrees@example.com
// Accessing a non-existent key returns null
print(user['phone']); // null
// Adding a new entry
user['phone'] = '+966555555555';
print(user['phone']); // +966555555555
// Updating an existing entry
user['age'] = '29';
print(user['age']); // 29
// Removing an entry
user.remove('email');
print(user); // {name: Edrees, age: 29, phone: +966555555555}
}
null, not an error. This means the return type of map[key] is always nullable (V?). Always check for null or use the ! operator only when you are certain the key exists.Essential Map Properties
Map Properties
void main() {
var fruits = {
'apple': 3,
'banana': 5,
'orange': 2,
};
print(fruits.length); // 3
print(fruits.isEmpty); // false
print(fruits.isNotEmpty); // true
print(fruits.keys); // (apple, banana, orange)
print(fruits.values); // (3, 5, 2)
print(fruits.entries); // (MapEntry(apple: 3), MapEntry(banana: 5), MapEntry(orange: 2))
}
Important Map Methods
Dart’s Map class provides a rich set of methods for querying and transforming data:
containsKey and containsValue
void main() {
var inventory = {'laptop': 10, 'phone': 25, 'tablet': 8};
print(inventory.containsKey('laptop')); // true
print(inventory.containsKey('desktop')); // false
print(inventory.containsValue(25)); // true
print(inventory.containsValue(100)); // false
}
putIfAbsent
void main() {
var settings = {'theme': 'dark', 'language': 'en'};
// Only adds if the key does not already exist
settings.putIfAbsent('theme', () => 'light');
print(settings['theme']); // dark (unchanged, key already existed)
settings.putIfAbsent('fontSize', () => '14');
print(settings['fontSize']); // 14 (added because key was absent)
print(settings); // {theme: dark, language: en, fontSize: 14}
}
update Method
void main() {
var stock = {'apples': 10, 'bananas': 5};
// Update an existing key
stock.update('apples', (value) => value + 5);
print(stock['apples']); // 15
// Update with ifAbsent -- adds the key if it does not exist
stock.update('grapes', (value) => value + 1, ifAbsent: () => 20);
print(stock['grapes']); // 20
print(stock); // {apples: 15, bananas: 5, grapes: 20}
}
forEach on Maps
void main() {
var prices = {'Coffee': 15.0, 'Tea': 10.0, 'Juice': 12.5};
prices.forEach((key, value) {
print('$key costs $value SAR');
});
// Coffee costs 15.0 SAR
// Tea costs 10.0 SAR
// Juice costs 12.5 SAR
}
map Method -- Transforming a Map
void main() {
var prices = {'Coffee': 15.0, 'Tea': 10.0, 'Juice': 12.5};
// Apply 15% VAT to all prices
var withVat = prices.map((key, value) {
return MapEntry(key, value * 1.15);
});
print(withVat);
// {Coffee: 17.25, Tea: 11.5, Juice: 14.375}
}
Other Useful Map Methods
void main() {
var scores = {'Ahmed': 85, 'Sara': 92, 'Ali': 78, 'Fatima': 95};
// addAll -- merge another map into this one
scores.addAll({'Omar': 88, 'Noor': 91});
print(scores.length); // 6
// removeWhere -- remove entries matching a condition
scores.removeWhere((key, value) => value < 80);
print(scores); // {Ahmed: 85, Sara: 92, Fatima: 95, Omar: 88, Noor: 91}
// clear -- remove all entries
scores.clear();
print(scores); // {}
}
Iterating Over Maps
There are several ways to loop through a map:
Different Iteration Approaches
void main() {
var countries = {'SA': 'Saudi Arabia', 'EG': 'Egypt', 'JO': 'Jordan'};
// 1. forEach method
countries.forEach((code, name) {
print('$code => $name');
});
// 2. for-in with entries
for (var entry in countries.entries) {
print('${entry.key} => ${entry.value}');
}
// 3. Iterate over keys only
for (var code in countries.keys) {
print('Code: $code');
}
// 4. Iterate over values only
for (var name in countries.values) {
print('Country: $name');
}
}
Nested Maps
Maps can contain other maps as values, which is common when working with structured data like JSON:
Nested Maps Example
void main() {
Map<String, Map<String, dynamic>> users = {
'user1': {
'name': 'Ahmed',
'age': 25,
'address': {
'city': 'Riyadh',
'country': 'Saudi Arabia',
},
},
'user2': {
'name': 'Sara',
'age': 30,
'address': {
'city': 'Cairo',
'country': 'Egypt',
},
},
};
// Accessing nested values
print(users['user1']!['name']); // Ahmed
var address = users['user1']!['address'] as Map;
print(address['city']); // Riyadh
}
Map<String, dynamic> structures.Sets: Unique Unordered Collections
A Set is a collection of unique values with no guaranteed order. Unlike Lists, a Set never contains duplicate elements. This makes Sets perfect for tracking unique items, membership tests, and mathematical set operations.
Creating Sets
Set Creation
void main() {
// Set literal with type annotation
Set<String> colors = {'red', 'green', 'blue'};
// Type-inferred set
var numbers = {1, 2, 3, 4, 5};
// Empty set (must specify type or use Set constructor)
Set<int> emptySet = {};
var anotherEmpty = <String>{};
// Set from a list (removes duplicates)
var listWithDupes = [1, 2, 2, 3, 3, 3, 4];
var uniqueNumbers = listWithDupes.toSet();
print(uniqueNumbers); // {1, 2, 3, 4}
// Set.from constructor
var fromList = Set<int>.from([10, 20, 20, 30]);
print(fromList); // {10, 20, 30}
}
var x = {}; creates an empty Map, not a Set. To create an empty Set, use var x = <Type>{}; or Set<Type> x = {};.Adding and Removing Elements
Set Modifications
void main() {
var tags = <String>{'dart', 'flutter'};
// add -- returns true if element was added (new)
bool added = tags.add('firebase');
print(added); // true
print(tags); // {dart, flutter, firebase}
// Adding a duplicate does nothing
added = tags.add('dart');
print(added); // false (already exists)
print(tags); // {dart, flutter, firebase}
// addAll -- add multiple elements
tags.addAll({'android', 'ios', 'dart'});
print(tags); // {dart, flutter, firebase, android, ios}
// remove -- returns true if element was removed
tags.remove('android');
print(tags); // {dart, flutter, firebase, ios}
// removeWhere
tags.removeWhere((tag) => tag.length > 5);
print(tags); // {dart, ios}
}
Set Membership and Properties
Checking Membership
void main() {
var permissions = {'read', 'write', 'execute'};
print(permissions.contains('read')); // true
print(permissions.contains('delete')); // false
print(permissions.length); // 3
print(permissions.isEmpty); // false
print(permissions.isNotEmpty); // true
// containsAll -- check if all elements exist
print(permissions.containsAll({'read', 'write'})); // true
print(permissions.containsAll({'read', 'admin'})); // false
}
contains() method on a Set runs in O(1) constant time, while on a List it runs in O(n) linear time. If you frequently check whether a value exists in a large collection, convert it to a Set for much better performance.Set Operations: Union, Intersection, and Difference
Sets support the classic mathematical set operations, which are incredibly useful for comparing and combining data:
Set Operations
void main() {
var frontend = {'HTML', 'CSS', 'JavaScript', 'Dart'};
var backend = {'PHP', 'Python', 'Dart', 'JavaScript'};
// Union -- all elements from both sets (no duplicates)
var allLanguages = frontend.union(backend);
print(allLanguages);
// {HTML, CSS, JavaScript, Dart, PHP, Python}
// Intersection -- elements in BOTH sets
var shared = frontend.intersection(backend);
print(shared); // {JavaScript, Dart}
// Difference -- elements in first set but NOT in second
var frontendOnly = frontend.difference(backend);
print(frontendOnly); // {HTML, CSS}
var backendOnly = backend.difference(frontend);
print(backendOnly); // {PHP, Python}
}
Removing Duplicates from a List
One of the most common uses of Sets is removing duplicates from a List:
Deduplication Pattern
void main() {
var votes = ['red', 'blue', 'red', 'green', 'blue', 'red', 'green'];
// Convert to Set to remove duplicates, then back to List
var uniqueVotes = votes.toSet().toList();
print(uniqueVotes); // [red, blue, green]
// Count unique voters
var voterIds = [101, 102, 101, 103, 102, 104];
var uniqueVoters = voterIds.toSet();
print('Total votes: ${voterIds.length}'); // 6
print('Unique voters: ${uniqueVoters.length}'); // 4
}
Practical Examples
Word Counter using Maps
Word Frequency Counter
void main() {
var text = 'the cat sat on the mat the cat';
var words = text.split(' ');
Map<String, int> wordCount = {};
for (var word in words) {
wordCount.update(word, (count) => count + 1, ifAbsent: () => 1);
}
print(wordCount);
// {the: 3, cat: 2, sat: 1, on: 1, mat: 1}
// Find the most frequent word
var mostFrequent = wordCount.entries
.reduce((a, b) => a.value > b.value ? a : b);
print('Most frequent: "${mostFrequent.key}" (${mostFrequent.value} times)');
// Most frequent: "the" (3 times)
}
Configuration Storage using Maps
App Configuration
void main() {
Map<String, dynamic> config = {
'appName': 'My App',
'version': '2.1.0',
'maxRetries': 3,
'debugMode': false,
'supportedLocales': ['en', 'ar', 'fr'],
'api': {
'baseUrl': 'https://api.example.com',
'timeout': 30,
},
};
// Safe access with null check
var appName = config['appName'] ?? 'Unknown';
print(appName); // My App
// Update a setting
config['debugMode'] = true;
// Add a new setting only if it does not exist
config.putIfAbsent('cacheEnabled', () => true);
print(config['cacheEnabled']); // true
}
Tracking Unique Items with Sets
Unique Page Visitors
void main() {
// Simulating page visits (some users visit multiple times)
var pageVisits = [
{'userId': 'u1', 'page': '/home'},
{'userId': 'u2', 'page': '/home'},
{'userId': 'u1', 'page': '/about'},
{'userId': 'u3', 'page': '/home'},
{'userId': 'u1', 'page': '/home'},
{'userId': 'u2', 'page': '/about'},
];
// Unique visitors per page
Map<String, Set<String>> uniqueVisitors = {};
for (var visit in pageVisits) {
var page = visit['page']!;
var user = visit['userId']!;
uniqueVisitors.putIfAbsent(page, () => <String>{});
uniqueVisitors[page]!.add(user);
}
uniqueVisitors.forEach((page, visitors) {
print('$page: ${visitors.length} unique visitors $visitors');
});
// /home: 3 unique visitors {u1, u2, u3}
// /about: 2 unique visitors {u1, u2}
}
When to Use Map vs Set vs List
Choosing the right collection type is an important design decision:
- List -- Use when order matters, duplicates are allowed, and you access elements by index. Example: a list of chat messages, an ordered queue of tasks.
- Map -- Use when you need to look up values by a unique key. Example: user profiles by ID, app configuration, word frequency counts.
- Set -- Use when you need unique values with fast membership checks. Example: tracking which items a user has seen, collecting unique tags, permissions.
Map<String, Set<String>> maps keys to sets of unique values. A List<Map<String, dynamic>> is essentially a list of records (similar to a database result set). Choose the combination that best models your data.Summary
- Maps store key-value pairs with
Map<K, V>syntax - Access map values with bracket notation:
map[key] - Key map methods:
containsKey,containsValue,putIfAbsent,update,forEach,map,addAll,removeWhere - Sets store unique values with
Set<T>syntax - Key set methods:
add,remove,contains,union,intersection,difference - Convert a List to a Set with
.toSet()to remove duplicates - Use Maps for lookups, Sets for uniqueness, Lists for ordered sequences
Practice Exercise
Open DartPad and build a small inventory management program. Create a Map<String, int> for product stock (e.g., 'laptop': 10, 'phone': 25). Write functions to: (1) add stock using update with ifAbsent, (2) remove a product, (3) list all products with stock below 5. Then create a Set<String> of categories. Add categories, check membership, and compute the union of two category sets. Print the results of each operation.