Collections, Iterables & Equality
Data Structures That Show Up in Every Interview
Open interactive version (quiz + challenge)Real-world analogy
Collections are like different kinds of bags. A List is a numbered shopping bag where order matters. A Set is a bag of unique marbles — try to add a duplicate and it just bounces off. A Map is a filing cabinet where every drawer has a label (key) and holds one thing (value).
What is it?
Dart collections (List, Set, Map) are the core data structures you'll use in every Flutter app and every interview. Understanding their performance characteristics, equality behavior, and functional methods (map/where/reduce) is essential for writing efficient code and answering algorithm questions.
Real-world relevance
In a SaaS collaboration app, you use Map to store workspace data by ID, Set to track unique online users, and List for ordered chat messages. The .where() method filters messages by channel, .map() transforms API responses into UI models, and proper equality ensures BLoC state changes trigger rebuilds correctly.
Key points
- List — Ordered, Indexed, Allows Duplicates — List is Dart's array. Zero-indexed, ordered, allows duplicates. Use [] literal or List.generate(). Growable by default. Fixed-length with List.filled(). Interview: Know the difference between growable and fixed-length lists.
- Set — Unique Values Only — Set stores unique values with O(1) lookup. Adding a duplicate does nothing. Great for tracking unique IDs, removing duplicates from a list (.toSet().toList()), and membership testing (.contains()). Interview favorite for deduplication questions.
- Map — Key-Value Pairs — Map stores key-value pairs. Keys must be unique. Access is O(1) average. Use map['key'] for lookup (returns null if missing), map.containsKey() for existence check. In production, used everywhere for JSON parsing and state modeling.
- Iterable and Lazy Evaluation — Iterable is the parent of List and Set. Methods like .map(), .where(), .expand() return lazy Iterables — they don't compute until you iterate. Call .toList() to force evaluation. Interview trap: lazy operations chain but only execute once consumed.
- map, where, reduce, fold — .map() transforms each element. .where() filters. .reduce() combines all into one value (needs non-empty list). .fold() is like reduce but takes an initial value (works on empty lists). These are functional programming essentials tested in interviews.
- spread (...) and collection if/for — Spread operator: [...list1, ...list2] merges lists. Collection if: [1, 2, if (showThree) 3]. Collection for: [for (var i in items) i.name]. These are Dart-specific syntactic sugar that interviewers love to test.
- Equality: == vs identical() — == checks value equality (can be overridden). identical() checks reference equality (same object in memory). For custom classes, override == and hashCode together. Interview: two objects can be == equal but not identical.
- Equatable Pattern — Override == and hashCode manually is error-prone. The equatable package auto-generates both from a list of props. Essential for BLoC states: if state objects aren't properly equatable, BLoC won't emit 'equal' states, causing UI bugs.
- Unmodifiable and Const Collections — List.unmodifiable() creates a runtime-immutable view. const [1,2,3] creates a compile-time constant. Both prevent modification. In Clean Architecture, returning unmodifiable collections from repositories prevents accidental mutation.
- Common Collection Patterns — Grouping: fold into Map>. Chunking: partition list into sublists. Flattening: expand() nested lists. Zipping: combine two lists element-by-element. Know these patterns — they appear in coding challenges.
Code example
// Collections — Interview Essentials
// List — ordered, indexed
final List<String> fruits = ['apple', 'banana', 'cherry'];
fruits.add('date'); // [apple, banana, cherry, date]
fruits.removeAt(1); // [apple, cherry, date]
final sliced = fruits.sublist(0, 2); // [apple, cherry]
// Set — unique values, O(1) lookup
final Set<int> ids = {1, 2, 3, 3, 3}; // {1, 2, 3} — duplicates ignored
ids.contains(2); // true — O(1)
final unique = [1,1,2,2,3].toSet().toList(); // [1, 2, 3]
// Map — key-value pairs
final Map<String, int> scores = {'math': 95, 'science': 87};
scores['english'] = 91; // Add entry
final math = scores['math'] ?? 0; // Safe access with fallback
scores.entries.forEach((e) => print('${e.key}: ${e.value}'));
// Functional operations (lazy by default)
final numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
final doubled = numbers.map((n) => n * 2); // Lazy Iterable
final evens = numbers.where((n) => n.isEven); // Lazy Iterable
final sum = numbers.reduce((a, b) => a + b); // 55
final product = numbers.fold<int>(1, (a, b) => a * b); // Works on empty too
// Force evaluation
final doubledList = doubled.toList(); // [2, 4, 6, 8, ...]
// Spread and collection if/for
final combined = [...fruits, ...['extra1', 'extra2']];
final bool showAdmin = true;
final menu = [
'Home',
'Profile',
if (showAdmin) 'Admin Panel',
];
final widgets = [for (var item in menu) 'MenuItem: $item'];
// Equality
class User {
final String id;
final String name;
const User(this.id, this.name);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is User && other.id == id && other.name == name;
@override
int get hashCode => id.hashCode ^ name.hashCode;
}
final u1 = User('1', 'Alice');
final u2 = User('1', 'Alice');
print(u1 == u2); // true (value equality)
print(identical(u1, u2)); // false (different instances)Line-by-line walkthrough
- 1. Creating an ordered List of strings with three items
- 2. Adding 'date' to the end of the list
- 3. Removing the item at index 1 (banana)
- 4. Getting a sublist from index 0 to 2 (exclusive)
- 5. Creating a Set — notice the duplicate 3s are automatically removed
- 6. Checking membership in O(1) time — much faster than List.contains()
- 7. Converting a list with duplicates to a Set and back to get unique values
- 8. Creating a Map with String keys and int values
- 9. Adding a new key-value pair using bracket notation
- 10. Safe access with ?? fallback — if key doesn't exist, returns 0
- 11. Iterating over map entries with forEach
- 12. Functional chain: .map() transforms lazily, .where() filters lazily
- 13. .reduce() combines all elements — throws on empty list
- 14. Using .fold() with initial value 1 — safe on empty lists
Spot the bug
final list = const [3, 1, 4, 1, 5];
list.sort();
final map = <String, int>{};
map['a'] = 1;
final value = map['b'];
print(value.isEven);Need a hint?
A const list can't be modified, and a missing map key returns null...
Show answer
Bug 1: list is const so .sort() throws at runtime — remove const to make it mutable. Bug 2: map['b'] returns null (key doesn't exist), so value is int? — calling .isEven on null crashes. Fix: print(value?.isEven ?? false) or check for null first.
Explain like I'm 5
Imagine you have three types of toy boxes. A List box has numbered slots — toy #1, toy #2, toy #3 — and you can have two identical toys. A Set box is magical — if you try to put in a toy you already have, it just bounces back out. A Map box has labeled drawers — the 'car' drawer, the 'doll' drawer — and each label opens to exactly one toy.
Fun fact
Dart's collection-if and collection-for syntax is unique among mainstream languages. Most languages require separate logic to conditionally include items in a list. Dart lets you do it inline, which is especially powerful when building widget trees in Flutter.
Hands-on challenge
Write a function that takes a List> of user JSON objects and returns a List of unique email addresses, sorted alphabetically. Use .map(), .toSet(), and .toList(). Handle null email fields gracefully.
More resources
- Dart Collections (Dart Official)
- Iterable API Reference (Dart Official)
- Dart Collection Methods (Dart Official)
- Equatable Package (pub.dev)