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. That is, every element of the list must also be a compile-time constant.

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. They make it easier for Flutter developers to compose user interface layouts completely in code, without the need for a separate markup language.

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;
const 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

  1. Create an empty list of type String. Name it months. Use the add method to add the names of the twelve months.
  2. Make an immutable list with the same elements as in Mini-exercise 1.
  3. 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 must 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'];

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

  1. Create a map with the following keys: name, profession, country and city. For the values, add your own information.
  2. You suddenly decide to move to Toronto, Canada. Programmatically update the values for country and city.
  3. 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.

Examples of higher order methods on collections
Oxectbug af qemnas ormuw nutgiwq iq saclemfaupt

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, (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 if you get stuck. These are available with the supplementary materials for this book.

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 and for 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 the Map type, performs an operation on each element of a collection and returns the results as an Iterable.
  • The where method filters an iterable collection based on a condition.
  • The reduce and fold 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2020 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.