Lists, Maps, Sets and Iterable
Three collections carry almost every Dart program: List for order, Set for
membership, Map for lookup. All three are generic, so the element type travels
with the collection and the compiler can check what you take out of it.
void main() {
final numbers = <int>[3, 1, 2]; // List<int>
final unique = ['a', 'b', 'a'].toSet(); // Set<String>, so two entries
final ages = <String, int>{'ada': 36, 'grace': 45}; // Map<String, int>
print(numbers);
print(unique);
print(ages);
// The literal types are inferred when you give it something to work with.
final inferred = [1, 2, 3];
print(inferred.runtimeType);
// An empty literal with no type argument is a Map, not a Set.
print(<String>{}.runtimeType);
print({}.runtimeType);
}
Lists
void main() {
final tasks = ['write', 'review'];
tasks.add('publish');
tasks.insert(0, 'plan');
tasks.remove('review');
print(tasks);
print(tasks.first);
print(tasks.last);
print(tasks.length);
print(tasks.indexOf('publish'));
print(tasks.contains('plan'));
// Slicing gives a new list; sublist's end is exclusive.
print(tasks.sublist(1, 3));
// sort mutates in place and returns nothing - a common surprise.
final scores = [88, 42, 95];
scores.sort();
print(scores);
scores.sort((a, b) => b.compareTo(a)); // descending
print(scores);
// A fixed-length list refuses to grow, which is occasionally what you want.
final fixed = List<int>.filled(3, 0);
try {
fixed.add(1);
} on UnsupportedError {
print('fixed-length lists do not grow');
}
}
Maps
void main() {
final stock = <String, int>{'apples': 3, 'pears': 0};
// Reading a key that is not there gives null, not an error - which is why
// the return type is int? and not int.
print(stock['apples']);
print(stock['plums']);
print(stock['plums'] ?? 0);
stock['plums'] = 7; // insert or overwrite
stock.putIfAbsent('apples', () => 99); // only if missing, so no change
print(stock);
// update is the one that edits an existing value safely.
stock.update('apples', (current) => current + 1, ifAbsent: () => 1);
print(stock['apples']);
print(stock.containsKey('pears'));
print(stock.keys.toList());
print(stock.values.toList());
for (final entry in stock.entries) {
print('${entry.key}: ${entry.value}');
}
// Insertion order is preserved: the default Map is a LinkedHashMap.
print(stock.keys.join(' -> '));
}
Building collections with if and for
Collection-if and collection-for let a literal describe a shape that would otherwise need a mutable variable and several statements.
void main() {
const isAdmin = true;
const extras = ['metrics', 'flags'];
final menu = [
'home',
'courses',
if (isAdmin) 'admin', // included only when the condition holds
for (final extra in extras) 'tools/$extra',
...['help', 'about'], // spread
];
print(menu);
// ...? spreads a list that may be null.
List<String>? maybeMore;
final combined = [...menu, ...?maybeMore];
print(combined.length);
// The same syntax builds maps and sets.
final flags = {
for (final extra in extras) extra: true,
};
print(flags);
}
Iterable is the vocabulary, and it is lazy
map, where, expand and friends are defined on Iterable, which is the
supertype of every collection. They do not do the work when you call them: they
describe it, and the work happens when something asks for elements.
void main() {
final numbers = [1, 2, 3, 4, 5, 6];
final result = numbers
.where((n) => n.isEven)
.map((n) => n * n)
.toList();
print(result);
print(numbers.fold<int>(0, (sum, n) => sum + n));
print(numbers.reduce((a, b) => a > b ? a : b));
print(numbers.any((n) => n > 5));
print(numbers.every((n) => n > 0));
print(numbers.take(2).toList());
print(numbers.skip(4).toList());
print(numbers.expand((n) => [n, -n]).take(4).toList());
// firstWhere throws when nothing matches, unless you say what to do instead.
print(numbers.firstWhere((n) => n > 4, orElse: () => -1));
print(numbers.firstWhere((n) => n > 99, orElse: () => -1));
}
Laziness is worth seeing, because it decides when your side effects happen.
void main() {
final source = [1, 2, 3];
final lazy = source.map((n) {
print(' mapping $n');
return n * 2;
});
print('nothing has run yet');
print('first: ${lazy.first}'); // maps 1 only
print('now the whole thing:');
print(lazy.toList()); // maps 1, 2 and 3
// And it re-runs every time you iterate, so cache with toList() when the
// work is expensive or the source can change.
print(lazy.length);
}
What to take away
List,SetandMapare generic; the type argument is checked for you.map[key]returnsV?- a missing key is null, not an error.sortmutates and returns nothing;sublistandwherereturn something new.- Collection-if, collection-for and
...build a literal in one expression. Iterablemethods are lazy: they run when iterated, and again on each pass.toList()is how you pay for the work once.