Maps in Dart — Key-Value Mastery, Performance, and Patterns
A deep dive into Dart Maps: how key-value lookup works, the different Map implementations, common patterns, and AI prompts to help you choose the right data structure.
Why Maps exist — associating data by identity
Lists let you access items by position (list[0]), but what if you need to access by identity — a user ID, a product SKU, a configuration key? You could search through a List, but that's O(n). Maps solve this elegantly.
A Map is essentially a Set of key-value pairs, where each key is unique. The hash table gives you O(1) lookup, insertion, and deletion — by key.
// List — must search to find user
var users = [user1, user2, user3];
var found = users.firstWhere((u) => u.id == 'user_123'); // O(n)
// Map — direct lookup by key
var users = {'user_123': user1, 'user_456': user2};
var found = users['user_123']; // O(1)
Map implementations — LinkedHashMap, HashMap, SplayTreeMap
Like Set, Dart offers multiple Map implementations. And like Set, the default might surprise you.
import 'dart:collection';
// Default — preserves insertion order
var linked = {'c': 1, 'a': 2, 'b': 3};
print(linked.keys); // (c, a, b)
// Pure hash — no order guarantee
var hash = HashMap<String, int>()..addAll({'c': 1, 'a': 2, 'b': 3});
// Sorted by key
var sorted = SplayTreeMap<String, int>()..addAll({'c': 1, 'a': 2, 'b': 3});
print(sorted.keys); // (a, b, c)
Common Map patterns — lookup tables, caching, counting
Maps are incredibly versatile. Here are the patterns you'll use most often.
The null-aware assignment trick:
// Common pattern: compute expensive value only once
var cache = <String, Data>{};
Data getData(String key) {
return cache[key] ??= expensiveCompute(key);
// ??= means: if null, compute and assign
}
Map gotchas — null values, missing keys, and empty {}
Maps have a few surprises that catch developers off guard.
Gotcha 1: Empty {} creates a Map, not a Set
var oops = {}; // Map<dynamic, dynamic>, NOT Set!
var set = <String>{}; // This is a Set
var map = <String, int>{}; // This is a Map
Gotcha 2: map[key] returns null for missing keys
var map = {'a': 1};
print(map['b']); // null, not an error!
// Use containsKey() to distinguish
if (map.containsKey('b')) {
// Key exists (value might be null)
} else {
// Key doesn't exist
}
Gotcha 3: Null values are valid
var map = {'a': null};
print(map['a']); // null
print(map['b']); // also null!
// These are different situations:
print(map.containsKey('a')); // true
print(map.containsKey('b')); // false
Gotcha 4: Modifying during iteration throws
var map = {'a': 1, 'b': 2, 'c': 3};
// BAD — ConcurrentModificationError
for (var key in map.keys) {
if (map[key] == 2) map.remove(key); // throws!
}
// GOOD — collect keys first, then remove
var toRemove = map.keys.where((k) => map[k] == 2).toList();
toRemove.forEach(map.remove);
Transforming Maps — map, where, and conversion
Maps support functional transformations, but the syntax differs slightly from Lists.
var prices = {'apple': 1.20, 'banana': 0.80, 'cherry': 2.50};
// Transform values
var doubled = prices.map((k, v) => MapEntry(k, v * 2));
// {'apple': 2.40, 'banana': 1.60, 'cherry': 5.00}
// Filter entries
var expensive = Map.fromEntries(
prices.entries.where((e) => e.value > 1.0)
);
// {'apple': 1.20, 'cherry': 2.50}
// Swap keys and values
var inverted = prices.map((k, v) => MapEntry(v, k));
// {1.20: 'apple', 0.80: 'banana', 2.50: 'cherry'}
// Convert to List of pairs
var pairs = prices.entries.toList();
// [MapEntry(apple: 1.20), MapEntry(banana: 0.80), ...]
Grouping data:
var words = ['apple', 'banana', 'apricot', 'blueberry'];
// Group by first letter
var grouped = <String, List<String>>{};
for (var word in words) {
(grouped[word[0]] ??= []).add(word);
}
// {'a': ['apple', 'apricot'], 'b': ['banana', 'blueberry']}
Performance considerations
Maps share the same hash table performance characteristics as Sets.
Operation complexity:
- map[key] — O(1) average
- map[key] = value — O(1) amortised
- map.remove(key) — O(1) average
- map.containsKey(key) — O(1) average
- map.containsValue(value) — O(n) (must scan all values!)
- Iteration — O(n)
Key insight: containsValue() is O(n) — it can't use hashing because values aren't indexed. If you need fast value lookup, maintain a reverse Map.
// Slow: O(n) value lookup
var userById = <String, User>{};
var found = userById.values.firstWhere((u) => u.email == email);
// Fast: O(1) with reverse index
var userById = <String, User>{};
var userByEmail = <String, User>{};
void addUser(User u) {
userById[u.id] = u;
userByEmail[u.email] = u; // reverse index
}
var found = userByEmail[email]; // O(1)
Maps and parallel computation
Like other collections, Maps can be processed in Isolates for CPU-intensive operations.
import 'dart:isolate';
// Process large Map in parallel
Future<Map<String, int>> processInParallel(
Map<String, String> data,
) async {
return await Isolate.run(() {
// Heavy computation in separate isolate
return data.map((k, v) => MapEntry(k, expensiveHash(v)));
});
}
// Merge results from multiple isolates
Future<Map<String, int>> parallelCount(List<String> words) async {
// Split into chunks
var chunks = splitIntoChunks(words, 4);
// Process chunks in parallel
var results = await Future.wait(
chunks.map((chunk) => Isolate.run(() => countWords(chunk)))
);
// Merge results
return mergeCountMaps(results);
}
When to parallelise Map operations:
- Large Maps (>10k entries)
- Expensive per-entry computations
- Bulk transformations
When NOT to parallelise:
- Simple lookups (already O(1))
- Small Maps (overhead exceeds benefit)
AI prompts — choosing the right Map strategy
Use these prompts with AI assistants to help choose optimal Map strategies for your use case.
Prompt 1 — Data structure selection:
I need to store [describe your data] in Dart.
Access patterns:
- [how you access: by ID, by name, by multiple keys]
- Frequency: [read-heavy, write-heavy, balanced]
- Size: [approximate number of entries]
Should I use Map, List, or multiple data structures?
If Map, which implementation? Do I need reverse indexes?
Prompt 2 — JSON handling:
I'm working with this JSON structure in Dart:
[paste your JSON or describe structure]
Help me:
1. Define proper Dart types (typed Maps vs classes)
2. Handle nullable values safely
3. Write type-safe accessors
4. Consider using code generation (json_serializable)?
Prompt 3 — Caching strategy:
I need to cache [describe what you're caching] in Dart.
Requirements:
- Cache size: [bounded/unbounded]
- Eviction policy: [LRU/TTL/none]
- Thread safety: [single isolate/multiple]
Design a Map-based caching solution with:
- Proper key design
- Memory management
- Cache invalidation strategy
Prompt 4 — Migration from dynamic Map:
Here is my Dart code using Map<String, dynamic>:
[paste your code]
Refactor this to be more type-safe. Consider:
- Creating model classes
- Using proper generic types
- Handling missing keys gracefully
- Adding null safety
Maps in Dart
Maps are the key-value workhorses of Dart development. This episode explores how they achieve O(1) lookup, common patterns from caching to counting, and the gotchas that trip up developers.
Maps Deep Dive Quiz
7 questions
Test your understanding of Dart Maps, hash tables, and patterns