Dart Collections — Lists, Sets, Maps, and Collection Operations
In this tutorial, you will learn about Dart Collections. We cover key concepts, practical examples, and best practices to help you master this topic.
Dart collections provide type-safe data structures including List, Set, and Map with concise literal syntax, spread operators, collection-if, and a rich set of built-in operations for filtering, mapping, and reducing data.
What You Will Learn
- Creating and manipulating Lists, Sets, and Maps
- Collection literals and type inference
- Spread (
...) and null-aware spread (...?) operators - Collection-if for conditional inclusion
- Common collection operations: map, where, reduce, fold
- Sorting, filtering, and transforming data
- Iterator and Iterable interface
Why It Matters
Collections are the primary way to work with groups of data in any program. Dart's collection system is designed to be concise and expressive. Collection literals with spread and collection-if reduce boilerplate compared to imperative building. The rich set of functional operations (map, where, reduce, fold) eliminate the need for explicit loops in most data transformation scenarios. Understanding collections is critical for Flutter development, where lists of widgets are built using collection operations.
Real-World Use
The DodaTech Flutter app uses collection operations extensively. A list of course modules is filtered with where, transformed with map, and sorted with sort. The search feature uses collection-if to conditionally include filters. The notification system uses set operations to merge and deduplicate notification IDs.
Learning Path
flowchart LR A[Dart Functions] --> B[Collections\nYou are here] B --> C[Dart Classes] style B fill:#f90,color:#fff
Lists
A List is an ordered collection of elements indexed by position. Dart lists are zero-indexed and growable by default:
void main() {
// List literal
var fruits = ['apple', 'banana', 'cherry'];
print('Fruits: $fruits');
print('First: ${fruits[0]}');
print('Length: ${fruits.length}');
// Adding and removing
fruits.add('date');
fruits.insert(1, 'blueberry');
fruits.remove('banana');
print('After mutations: $fruits');
// Type-safe list
List<int> numbers = [1, 2, 3, 4, 5];
// numbers.add('text'); // COMPILE ERROR
// Empty list with type
var empty = <String>[];
print('Empty list: $empty');
}
Output:
Fruits: [apple, banana, cherry]
First: apple
Length: 3
After mutations: [apple, blueberry, cherry, date]
Empty list: []
List literals use square brackets. The [] subscript accesses elements. add, insert, remove, and removeAt are common mutation methods.
Sets
A Set is an unordered collection of unique elements. Duplicate values are automatically ignored:
void main() {
// Set literal
var unique = {'apple', 'banana', 'apple', 'cherry'};
print('Unique fruits: $unique');
// Adding and checking
unique.add('date');
unique.add('banana'); // Already present, ignored
print('Has apple? ${unique.contains('apple')}');
print('Size: ${unique.length}');
// Set operations
var setA = {1, 2, 3, 4};
var setB = {3, 4, 5, 6};
print('Union: ${setA.union(setB)}');
print('Intersection: ${setA.intersection(setB)}');
print('Difference A-B: ${setA.difference(setB)}');
}
Output:
Unique fruits: {apple, banana, cherry}
Has apple? true
Size: 4
Union: {1, 2, 3, 4, 5, 6}
Intersection: {3, 4}
Difference A-B: {1, 2}
Sets use curly braces. The contains method is O(1) for hash-based sets. Set operations like union, intersection, and difference produce new sets without mutating the originals.
Maps
A Map is a collection of key-value pairs. Keys are unique, and each key maps to exactly one value:
void main() {
// Map literal
var scores = {
'Alice': 95,
'Bob': 87,
'Charlie': 92,
};
print('Scores: $scores');
// Accessing values
print("Alice's score: ${scores['Alice']}");
print("Unknown: ${scores['Unknown']}"); // null
// Adding and updating
scores['Diana'] = 88; // Add
scores['Bob'] = 90; // Update
print('Updated: $scores');
// Checking existence
print('Contains Alice? ${scores.containsKey('Alice')}');
print('Contains value 100? ${scores.containsValue(100)}');
// Iterating
scores.forEach((name, score) {
print('$name: $score');
});
}
Output:
Scores: {Alice: 95, Bob: 87, Charlie: 92}
Alice's score: 95
Unknown: null
Updated: {Alice: 95, Bob: 90, Charlie: 92, Diana: 88}
Contains Alice? true
Contains value 100? false
Alice: 95
Bob: 90
...
Accessing a missing key returns null. The putIfAbsent method adds a value only if the key is not present. Entries can be iterated with forEach or with entries property.
Spread Operator
The spread operator ... and null-aware spread ...? expand collections into another collection:
void main() {
var list1 = [1, 2, 3];
var list2 = [4, 5, 6];
// Spread operator
var combined = [...list1, ...list2];
print('Combined: $combined');
// Multiple spreads
var more = [0, ...list1, ...list2, 7];
print('More: $more');
// Null-aware spread
List<int>? nullableList = null;
var safe = [1, 2, ...?nullableList, 3];
print('Safe: $safe');
}
Output:
Combined: [1, 2, 3, 4, 5, 6]
More: [0, 1, 2, 3, 4, 5, 6, 7]
Safe: [1, 2, 3]
The spread operator copies elements from one collection into another. The null-aware variant (...?) safely handles null collections by ignoring them instead of throwing.
Collection-If
Collection-if conditionally includes elements in a collection literal:
void main() {
bool includeExtra = true;
bool includeWarning = false;
var items = [
'Home',
'Profile',
if (includeExtra) 'Settings',
if (includeWarning) 'Warning',
'Logout',
];
print('Menu items: $items');
// Collection-for
var numbers = [1, 2, 3];
var doubled = [
for (var n in numbers) n * 2,
];
print('Doubled: $doubled');
}
Output:
Menu items: [Home, Profile, Settings, Logout]
Doubled: [2, 4, 6]
Collection-if is useful for conditionally showing UI elements in Flutter. Collection-for provides a concise way to transform elements inline.
Common Collection Operations
Dart's Iterable interface provides a rich set of methods for data processing:
void main() {
var numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Filter with where
var evens = numbers.where((n) => n.isEven);
print('Evens: $evens');
// Transform with map
var squared = numbers.map((n) => n * n);
print('First 3 squares: ${squared.take(3).toList()}');
// Reduce to single value
var sum = numbers.reduce((a, b) => a + b);
print('Sum: $sum');
// Fold (like reduce but with initial value)
var product = numbers.fold(1, (acc, n) => acc * n);
print('Product of first 5: ${numbers.take(5).fold(1, (a, b) => a * b)}');
// Check conditions
print('All positive? ${numbers.every((n) => n > 0)}');
print('Any negative? ${numbers.any((n) => n < 0)}');
// Find elements
print('First > 5: ${numbers.firstWhere((n) => n > 5)}');
print('Last < 5: ${numbers.lastWhere((n) => n < 5)}');
}
Output:
Evens: (2, 4, 6, 8, 10)
First 3 squares: [1, 4, 9]
Sum: 55
Product of first 5: 120
All positive? true
Any negative? false
First > 5: 6
Last < 5: 4
where returns a lazy iterable (not a list). Convert to list with .toList() if you need indexed access. reduce throws on empty collections; use fold when the collection may be empty.
Sorting
Lists can be sorted in place or produce a sorted copy:
void main() {
var numbers = [3, 1, 4, 1, 5, 9, 2, 6];
numbers.sort();
print('Sorted: $numbers');
var descending = [3, 1, 4, 1, 5, 9];
descending.sort((a, b) => b.compareTo(a));
print('Descending: $descending');
// Sorting objects
var people = [
Person('Alice', 30),
Person('Bob', 25),
Person('Charlie', 35),
];
people.sort((a, b) => a.age.compareTo(b.age));
print('Youngest: ${people.first.name}');
print('Oldest: ${people.last.name}');
}
class Person {
final String name;
final int age;
Person(this.name, this.age);
}
Output:
Sorted: [1, 1, 2, 3, 4, 5, 6, 9]
Descending: [9, 5, 4, 3, 1, 1]
Youngest: Bob
Oldest: Charlie
The default sort() uses the natural ordering of elements. Custom comparators are functions that take two elements and return a negative, zero, or positive integer.
Iterating Collections
Dart provides multiple ways to iterate collections:
void main() {
var fruits = ['apple', 'banana', 'cherry'];
// For-in loop
for (var fruit in fruits) {
print(fruit);
}
// forEach with callback
fruits.forEach((fruit) => print('Got: $fruit'));
// While loop with iterator
var iterator = fruits.iterator;
while (iterator.moveNext()) {
print('Via iterator: ${iterator.current}');
}
}
Output:
apple
banana
cherry
Got: apple
Got: banana
Got: cherry
Via iterator: apple
Via iterator: banana
Via iterator: cherry
The for-in loop is the most idiomatic. forEach is useful for side effects. The manual iterator approach is rarely needed but demonstrates the underlying pattern.
Common Mistakes
Modifying a list while iterating with for-in: Adding or removing elements during iteration throws
ConcurrentModificationError. Collect items to modify separately, then apply changes after the loop.Using
List.fromwhen a cast is needed:List.from(iterable)creates a new list. For type conversion, uselist.cast<NewType>()orList<NewType>.from(iterable).Confusing
wherewithmap:wherefilters elements (keeps or discards).maptransforms each element. They are often chained:list.where(...).map(...).Forgetting that
wherereturns a lazy iterable: Callingwheredoes not evaluate the predicate until the result is iterated. Convert to list with.toList()if you need the results immediately or multiple times.Using
==on lists for equality:==checks reference equality, not element equality. Useconst [1, 2].equals([1, 2])with thecollectionpackage or compare element by element.
Practice Questions
- What is the difference between
List,Set, andMap? - How does the spread operator
...differ from...?? - When would you use
reducevsfold? - Why does
wherereturn anIterableinstead of aList? - Challenge: Write a Dart function that takes a list of integers and returns a map where keys are the unique values and values are the count of occurrences. Use collection operations without explicit loops.
Mini Project
Build a contact manager using collections:
- Store contacts in a
Map<String, String>(name to phone number) - Implement
addContact,removeContact, andsearchByNamefunctions - Implement
allContactsthat returns a sorted list of names - Implement
filterByPrefixthat returns contacts whose names start with a given prefix - Implement
mergeContactsthat combines two contact maps using spread - Write tests for all functions
FAQ
What is Next
Now that you can work with collections, learn how to model data with classes. Proceed to Classes in Dart for constructors, fields, methods, and inheritance. Then explore Dart Inheritance for subclasses and polymorphism.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro