8
Collections
Written by Jonathan Sande
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
In almost every application you make, you’ll be dealing with collections of data. Data can be organized in multiple ways, each with a different purpose. Dart provides multiple solutions to fit your collection’s needs, and in this chapter you’ll learn about three of the main ones: lists, sets and maps.
Lists
Whenever you have a very large collection of objects of a single type that have an ordering associated with them, you’ll likely want to use a list as the data structure for ordering the objects. Lists in Dart are similar to arrays in other languages.
The image below represents a list with six elements. Lists are zero-based, so the first element is at index 0
. The value of the first element is cake
, the value of the second element is pie
, and so on until the last element at index 5
, which is cookie
.
The order of a list matters. Pie is after cake, but before donut. If you loop through the list multiple times, you can be sure the elements will stay in the same location and order.
Basic list operations
You’ll start by learning how to create lists and modify elements.
Creating a list
You can create a list by specifying the initial elements of the list within square brackets. This is called a list literal.
var desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
desserts = [];
var snacks = [];
List<String> snacks = [];
var snacks = <String>[];
Printing lists
As you can do with any collection, you can print the contents of a list by using the print
statement. Since desserts
is currently empty, give it a list with elements again so that you have something interesting to show when you print it out.
desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
print(desserts);
[cookies, cupcakes, donuts, pie]
Accessing elements
To access the elements of a list, you reference its index via subscript notation, where the index number goes within square brackets after the list name.
final secondElement = desserts[1];
print(secondElement);
final index = desserts.indexOf('pie');
final value = desserts[index];
Assigning values to list elements
Just as you access elements, you also assign values to specific elements using subscript notation:
desserts[1] = 'cake';
Adding elements to a list
Lists are growable by default in Dart, so you can use the add
method to add an element.
desserts.add('brownies');
print(desserts);
[cookies, cake, donuts, pie, brownies]
Removing elements from a list
You can remove elements using the remove
method. So if you’d gotten a little hungry and eaten the cake, you’d write:
desserts.remove('cake');
print(desserts);
[cookies, donuts, pie, brownies]
Mutable and immutable lists
In the examples above, you were able to reassign list literals to desserts like so:
var desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
desserts = [];
desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
final desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
desserts = []; // not allowed
desserts = ['cake', 'ice cream']; // not allowed
desserts = someOtherList; // not allowed
final desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
desserts.remove('cookies'); // OK
desserts.remove('cupcakes'); // OK
desserts.add('ice cream'); // OK
The House on Wenderlich Way
You live in a house at 321 Lonely Lane. All you have at home are a few brownies, which you absentmindedly munch on as you scour the internet in hopes of finding work. Finally, you get a job as a senior Flutter developer, so you buy a new house at 122 Wenderlich Way. Best of all, your neighbor Ray brings over some cookies, cupcakes, donuts and pie as a house warming gift! The brownies are still at your old place, but in your excitement about the move you’ve forgotten all about them.
Creating deeply immutable lists
The solution to creating an immutable list is to mark the variable name with the const
keyword. This forces the list to be deeply immutable.
const desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
desserts.add('brownie'); // not allowed
desserts.remove('pie'); // not allowed
desserts[0] = 'fudge'; // not allowed
final desserts = const ['cookies', 'cupcakes', 'donuts', 'pie'];
final modifiableList = [DateTime.now(), DateTime.now()];
final unmodifiableList = List.unmodifiable(modifiableList);
List properties
Collections such as List
have a number of properties. To demonstrate them, use the following list of drinks.
const drinks = ['water', 'milk', 'juice', 'soda'];
Accessing first and last elements
You can access the first and last element in a list:
drinks.first // water
drinks.last // soda
Checking if a list contains any elements
You can also check whether a list is empty or not empty.
drinks.isEmpty // false
drinks.isNotEmpty // true
drinks.length == 0 // false
drinks.length > 0 // true
Looping over the elements of a list
For this section you can return to your list of desserts:
const desserts = ['cookies', 'cupcakes', 'donuts', 'pie'];
for (var dessert in desserts) {
print(dessert);
}
desserts.forEach((dessert) => print(dessert));
desserts.forEach(print);
cookies
cupcakes
donuts
pie
Code as UI
The Flutter framework chose Dart because of its unique characteristics. However, Flutter has also influenced the development of Dart. One area you can see this is with the addition of the spread operator, collection if
and collection for
.
Spread operator
Suppose you have two lists to start with.
const pastries = ['cookies', 'croissants'];
const candy = ['Junior Mints', 'Twizzlers', 'M&Ms'];
const desserts = ['donuts', ...pastries, ...candy];
print(desserts);
[donuts, cookies, croissants, Junior Mints, Twizzlers, M&Ms]
List<String>? coffees;
final hotDrinks = ['milk tea', ...?coffees];
Collection if
When creating a list, you can use a collection if
to determine whether an element is included based on some condition.
const peanutAllergy = true;
const candy = [
'Junior Mints',
'Twizzlers',
if (!peanutAllergy) 'Reeses',
];
print(candy);
[Junior Mints, Twizzlers]
Collection for
There’s also a collection for
. So if you have a list, you can use a collection for
to iterate over the list and generate another list.
const deserts = ['gobi', 'sahara', 'arctic'];
var bigDeserts = [
'ARABIAN',
for (var desert in deserts) desert.toUpperCase(),
];
print(bigDeserts);
[ARABIAN, GOBI, SAHARA, ARCTIC]
Mini-exercises
- Create an empty list of type
String
. Name itmonths
. Use theadd
method to add the names of the twelve months. - Make an immutable list with the same elements as in Mini-exercise 1.
- Use collection
for
to create a new list with the month names in all uppercase.
Sets
Sets are used to create a collection of unique elements. Sets in Dart are similar to their mathematical counterparts. Duplicates are not allowed in a set, in contrast to lists, which do allow duplicates.
Creating a set
You can create an empty set in Dart using the Set
type annotation like so:
final Set<int> someSet = {};
final someSet = <int>{};
final anotherSet = {1, 2, 3, 1};
print(anotherSet);
{1, 2, 3}
Operations on a set
In this section you’ll see some general collection operations that also apply to sets.
Checking the contents
To see if a set contains an item, you use the contains
method, which returns a bool
.
print(anotherSet.contains(1)); // true
print(anotherSet.contains(99)); // false
Adding single elements
Like growable lists, you can add and remove elements in a set. To add an element, use the add
method.
final someSet = <int>{};
someSet.add(42);
someSet.add(2112);
someSet.add(42);
print(someSet);
{42, 2112}
Removing elements
You can also remove elements using the remove
method.
someSet.remove(2112);
{42}
Adding multiple elements
You can use addAll
to add elements from a list into a set.
someSet.addAll([1, 2, 3, 4]);
{42, 1, 2, 3, 4}
Intersections and Unions
You’ll often have multiple sets of data and you’ll want to know how they fit together.
Intersections
Like Venn diagrams and mathematical sets, you can find the intersection of two sets in Dart; that is, the common elements that occur in both sets.
final setA = {8, 2, 3, 1, 4};
final setB = {1, 6, 5, 4};
final intersection = setA.intersection(setB);
{1, 4}
Unions
Finding all the unique values by combining both sets gives you the union, and that’s just as easy to find in Dart as the intersection.
final union = setA.union(setB);
{8, 2, 3, 1, 4, 6, 5}
Other operations
Almost everything that you learned earlier about lists also applies to sets. Specifically, you can perform any of the following operations with sets:
Maps
Maps in Dart are the data structure used to hold key-value pairs. They’re similar to HashMaps and Dictionaries in other languages.
Creating an empty map
Like List
and Set
, Map
is a generic type, but Map
takes two type parameters: one for the key and one for the value. You can create an empty map variable using Map
and specifying the type for both the key and value:
final Map<String, int> emptyMap = {};
final emptyMap = <String, int>{};
final emptySomething = {};
final mySet = <String>{};
final emptyMap = <String, int>{};
print(emptyMap.length);
Initializing a Map with values
You can create a non-empty map variable using braces, where Dart infers the key and value types. Dart knows it’s a map because each element is a pair separated by a colon.
final inventory = {
'cakes': 20,
'pies': 14,
'donuts': 37,
'cookies': 141,
};
final digitToWord = {
1: 'one',
2: 'two',
3: 'three',
4: 'four',
};
print(inventory);
print(digitToWord);
{cakes: 20, pies: 14, donuts: 37, cookies: 141}
{1: one, 2: two, 3: three, 4: four}
Unique keys
The keys of a map should be unique. A map like the following wouldn’t work:
final treasureMap = {
'garbage': 'in the dumpster',
'glasses': 'on your head',
'gold': 'in the cave',
'gold': 'under your mattress',
};
final treasureMap = {
'garbage': ['in the dumpster'],
'glasses': ['on your head'],
'gold': ['in the cave', 'under your mattress'],
};
final myHouse = {
'bedroom': 'messy',
'kitchen': 'messy',
'living room': 'messy',
'code': 'clean',
};
Operations on a map
Interacting with a map to access, add, remove and update elements is very similar to what you’ve already seen.
Accessing elements from a map
You access individual elements from a map by using a subscript notation similar to lists, except for maps you use the key rather than an index.
final numberOfCakes = inventory['cakes'];
print(numberOfCakes?.isEven);
Adding elements to a map
You can add new elements to a map simply by assigning to elements that are not yet in the map.
inventory['brownies'] = 3;
{cakes: 20, pies: 14, donuts: 37, cookies: 141, brownies: 3}
Updating an element
Remember that the keys of a map are unique, so if you assign a value to a key that already exists, you’ll overwrite the existing value.
inventory['cakes'] = 1;
{cakes: 1, pies: 14, donuts: 37, cookies: 141, brownies: 3}
Removing elements from a map
You can use remove
to remove elements from a map by key.
inventory.remove('cookies');
{cakes: 1, pies: 14, donuts: 37, brownies: 3}
Map properties
Maps have properties just as lists do. For example, the following properties indicate (using different metrics) whether or not the map is empty:
inventory.isEmpty // false
inventory.isNotEmpty // true
inventory.length // 4
print(inventory.keys);
print(inventory.values);
(cakes, pies, donuts, brownies)
(1, 14, 37, 3)
Checking for key or value existence
To check whether a key is in a map, you can use the containsKey
method:
print(inventory.containsKey('pies'));
// true
print(inventory.containsValue(42));
// false
Looping over elements of a map
Unlike lists, you can’t iterate over a map using a for-in
loop.
for (var item in inventory) {
print(inventory[item]);
}
The type 'Map<String, int>' used in the 'for' loop must implement Iterable.
for (var item in inventory.keys) {
print(inventory[item]);
}
inventory.forEach((key, value) => print('$key -> $value'));
for (final entry in inventory.entries) {
print('${entry.key} -> ${entry.value}');
}
cakes -> 1
pies -> 14
donuts -> 37
brownies -> 3
Mini-exercises
- Create a map with the following keys:
name
,profession
,country
andcity
. For the values, add your own information. - You suddenly decide to move to Toronto, Canada. Programmatically update the values for
country
andcity
. - Iterate over the map and print all the values.
Higher order methods
There are a number of collection operations common to many programming languages, including transforming, filtering and consolidating the elements of the collection. These operations are known as higher order methods, because they take functions as parameters. This is a great opportunity to apply what you learned in Chapter 5 about anonymous functions.
Mapping over a collection
Mapping over a collection allows you to perform an action on each element of the collection as if you were running it through a loop. To do this, collections have a map
method that takes an anonymous function as a parameter, and returns another collection based on what the function does to the elements.
const numbers = [1, 2, 3, 4];
final squares = numbers.map((number) => number * number);
(1, 4, 9, 16)
print(squares.toList());
[1, 4, 9, 16]
Filtering a collection
You can filter an iterable collection like List
and Set
down to another shorter collection by using the where
method.
final evens = squares.where((square) => square.isEven);
(4, 16)
Consolidating a collection
Some higher order methods take all the elements of an iterable collection and consolidate them into a single value using the function you provide. You’ll learn two ways to do this.
Using reduce
One way to combine all of the elements of a list into a single value is to use the reduce
method. You can combine the elements in any way you like, but the example below shows how to find their sum.
const amounts = [199, 299, 299, 199, 499];
final total = amounts.reduce((sum, element) => sum + element);
Using fold
If you try to call reduce
on an empty list, you’ll get an error. For that reason, using fold
may be more reliable when a collection has a possibility of containing zero elements. The fold
method works like reduce
, but it takes an extra parameter that provides the function with a starting value.
const amounts = [199, 299, 299, 199, 499];
final total = amounts.fold(
0,
(int sum, element) => sum + element,
);
Sorting a list
While where
, reduce
and fold
all work equally well on lists or sets, you can only call sort
on a list. That’s because sets are by definition unordered, so it wouldn’t make sense to sort them.
final desserts = ['cookies', 'pie', 'donuts', 'brownies'];
desserts.sort();
[brownies, cookies, donuts, pie]
Reversing a list
You can use reversed
to produce a list in reverse order.
var dessertsReversed = desserts.reversed;
(pie, donuts, cookies, brownies)
final desserts = ['cookies', 'pie', 'donuts', 'brownies'];
final dessertsReversed = desserts.reversed;
print(desserts);
print(dessertsReversed);
[cookies, pie, donuts, brownies]
(brownies, donuts, pie, cookies)
Performing a custom sort
For the sort
method, you can pass in a function as an argument to perform custom sorting. Say you want to sort strings by length and not alphabetically; you could give sort an anonymous function like so:
desserts.sort((d1, d2) => d1.length.compareTo(d2.length));
[pie, donuts, cookies, brownies]
Combining higher order methods
You can chain together the higher order methods that you learned above. For example, if you wanted to take only the desserts that have a name length greater than 5
and then convert those names to uppercase, you would do it like so:
const desserts = ['cake', 'pie', 'donuts', 'brownies'];
final bigTallDesserts = desserts
.where((dessert) => dessert.length > 5)
.map((dessert) => dessert.toUpperCase());
(DONUTS, BROWNIES)
Mini-exercises
Given the following exam scores:
final scores = [89, 77, 46, 93, 82, 67, 32, 88];
When to use lists, sets or maps
Congratulations on making it through another chapter! You’ve made a ton of progress. This chapter will leave you with some advice about when to use which type of collection. Each type has its strengths.
Challenges
Before moving on, here are some challenges to test your knowledge of collections. It’s best if you try to solve them yourself, but solutions are available with the supplementary materials for this book if you get stuck.
Challenge 1: A unique request
Write a function that takes a paragraph of text and returns a collection of unique String
characters that the text contains.
Challenge 2: Counting on you
Repeat Challenge 1, but this time have the function return a collection that contains the frequency, or count, of every unique character.
Challenge 3: Mapping users
Create a class called User
with properties for id
and name
. Make a List
with three users, specifying any appropriate names and IDs you like. Then write a function that converts your user list to a list of maps whose keys are id
and name
.
Key points
- Lists store an ordered collection of elements.
- Sets store an unordered collection of unique elements.
- Maps store a collection of key-value pairs.
- The elements of a collection are mutable by default.
- The spread operator (
...
) allows you to expand one collection inside another collection. - Collection
if
andfor
can be used to dynamically create the content of a list or set. - You can iterate over any collection, but for a map you need to iterate over the keys or values if you use a
for-in
loop. - Higher order methods take a function as a parameter and act on the elements of a collection.
- The
map
method, not to be confused with theMap
type, performs an operation on each element of a collection and returns the results as anIterable
. - The
where
method filters an iterable collection based on a condition. - The
reduce
andfold
methods consolidate a collection down to a single value. - The
sort
method sorts a list in place according to its data type.
Where to go from here?
As jam-packed as this chapter was, it still didn’t include all there is to know about collections! For example, another collection type that Dart has is Queue
, which is a first-in, first-out data structure. This chapter also didn’t go into great detail about iterables. To explore more about collections and their methods in Dart, you can browse the contents of the dart:collection
library.