Chapters

Hide chapters

Dart Apprentice: Fundamentals

First Edition · Flutter · Dart 2.18 · VS Code 1.71

Dart Apprentice: Fundamentals

Section 1: 16 chapters
Show chapters Hide chapters

14. Maps
Written by Jonathan Sande

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

There are no streets or lakes or countries on these maps. Maps in Dart are the data structure used to hold key-value pairs. They’re like hash maps and dictionaries in other languages.

If you’re not familiar with maps, think of them as collections of variables containing data. The key is the variable name, and the value is the data the variable holds. To find a particular value, give the map the name of the key mapped to that value.

In the image below, the cake is mapped to 500 calories and the donut is mapped to 150 calories. cake and donut are keys, whereas 500 and 150 are values.

Colons separate the key and value in each pair, and commas separate consecutive key-value pairs.

Creating a 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 = {};

In this example, String is the type for the key, and int is the type for the value.

A slightly shorter way to do the same thing is to move the generic types to the right side:

final emptyMap = <String, int>{};

Notice that maps also use curly braces just as sets do. What do you think you’d get if you wrote this?

final emptySomething = {};

Is emptySomething a set or a map?

It turns out map literals came before set literals in Dart’s history, so Dart infers the empty braces to be a Map of <dynamic, dynamic>. In other words, the types of the key and value are both dynamic. If you want a set rather than a map, then you must be explicit:

final mySet = <String>{};

The single String type inside the angle brackets clarifies that this is a set and not a map.

Initializing a Map With Values

You can initialize a map with values by supplying the key-value pairs within the braces. This is known as a map literal.

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

Dart makes it easy to access, add, remove, update and iterate over the key-value pairs in a map.

final inventory = {
  'cakes': 20,
  'pies': 14,
  'donuts': 37,
  'cookies': 141,
};

Accessing Key-Value Pairs

You can look up the value of a particular key in a map using a subscript notation similar to that of lists. For maps, though, 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 them to keys 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 overwrite the existing value.

inventory['cakes'] = 1;
{cakes: 1, pies: 14, donuts: 37, cookies: 141, brownies: 3}

Removing Elements From a Map

Use remove to delete elements from a map by key.

inventory.remove('cookies');
{cakes: 1, pies: 14, donuts: 37, brownies: 3}

Accessing Properties

Maps have properties just as lists do. For example, the following properties indicate whether 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 or sets, you can’t directly 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 key in inventory.keys) {
  print(inventory[key]);
}
for (final entry in inventory.entries) {
  print('${entry.key} -> ${entry.value}');
}
cakes -> 1
pies -> 14
donuts -> 37
brownies -> 3

Exercise

  1. Create a map with the following keys: name, profession, country and city. For the values, add your information.
  2. You decide to move to Toronto, Canada. Programmatically update the values for country and city.
  3. Iterate over the map and print all the values.

Maps, Classes and JSON

In Chapter 8, “Classes”, you learned how JSON is used as a format to convert objects into strings. This is known as serialization, and it allows you to send objects over the network or save them in local storage. The JSON format is quite close to the structure of Dart maps, so you’ll often use maps as an intermediary data structure when converting between JSON and Dart objects.

Converting an Object to a Map

Add the following Dart class to your project:

class User {
  const User({
    required this.id,
    required this.name,
    required this.emails,
  });

  final int id;
  final String name;
  final List<String> emails;
}
final userObject = User(
  id: 1234,
  name: 'John',
  emails: [
    'john@example.com',
    'jhagemann@example.com',
  ],
);
final userMap = {
  'id': 1234,
  'name': 'John',
  'emails': [
    'john@example.com',
    'jhagemann@example.com',
  ],
};
Map<String, dynamic> toJson() {
  return <String, dynamic>{
    'id': id,
    'name': name,
    'emails': emails,
  };
}
final userMap = userObject.toJson();

Converting a Map to a JSON String

It would be a chore to build a JSON string by hand. Thankfully, the dart:convert library already has the map-to-JSON converter built-in.

import 'dart:convert';
final userString = jsonEncode(userMap);
print(userString);
{"id":1234,"name":"John","emails":["john@example.com","jhagemann@example.com"]}
{
  "id": 1234,
  "name": "John",
  "emails": [
    "john@example.com",
    "jhagemann@example.com"
  ]
}

Converting a JSON String to a Map

In Dart Apprentice: Beyond the Basics, Chapter 12, “Futures”, you’ll get some firsthand experience retrieving an actual JSON string from a server on the internet. For now, though, you’ll practice with the hard-coded sample string below.

final jsonString =
  '{"id":4321,"name":"Marcia","emails":["marcia@example.com"]}';
final jsonMap = jsonDecode(jsonString);

Preventing Hidden Dynamic Types

You can keep yourself from forgetting about dynamic by adding a setting to your analysis options file. Open analysis_options.yaml in the root of your project. Then, add the following lines at the bottom of the file and save your changes:

analyzer:
  strong-mode:
    implicit-dynamic: false

Explicitly Writing Dynamic

Back in your Dart file with the main function, the compiler is complaining at you now:

Missing variable type for 'jsonMap'.
Try adding an explicit type or removing implicit-dynamic from your analysis options file.
dynamic jsonMap = jsonDecode(jsonString);

Handling Errors

You can do a little error checking on the type using the is keyword:

if (jsonMap is Map<String, dynamic>) {
  print("You've got a map!");
} else {
  print('Your JSON must have been in the wrong format.');
}
You've got a map!

Converting a Map to an Object

You could, at this point, extract all the data from your map and pass it into the User constructor. However, that’s error-prone if you have to create many objects. Earlier, you added a toJson method. Now you’ll add a fromJson constructor to go the other way.

factory User.fromJson(Map<String, dynamic> jsonMap) {
  // 1
  dynamic id = jsonMap['id'];
  if (id is! int) id = 0;
  // 2
  dynamic name = jsonMap['name'];
  if (name is! String) name = '';
  // 3
  dynamic maybeEmails = jsonMap['emails'];
  final emails = <String>[];
  if (maybeEmails is List) {
    for (dynamic email in maybeEmails) {
      if (email is String) emails.add(email);
    }
  }
  // 4
  return User(
    id: id,
    name: name,
    emails: emails,
  );
}
@override
String toString() {
  return 'User(id: $id, name: $name, emails: $emails)';
}
final userMarcia = User.fromJson(jsonMap);
print(userMarcia);
User(id: 4321, name: Marcia, emails: [marcia@example.com])

Challenges

Before moving on, here are some challenges to test your knowledge of maps. It’s best if you try to solve them yourself, but solutions are available with the book’s supplementary materials if you get stuck.

Challenge 1: Counting on You

Write a function that takes a paragraph of text as a parameter. Count the frequency of each character. Return this data as a map where the key is the character and the value is the frequency count.

Challenge 2: To JSON and Back

Create an object from the following class:

class Widget {
  Widget(this.width, this.height);
  final double width;
  final double height;
}

Key Points

  • Maps store a collection of key-value pairs.
  • Using a key to access its value always returns a nullable type.
  • If you use a for-in loop with a map, you need to iterate over the keys or values.
  • You can convert a Dart object to a map by making the property names from the object the keys of the map.
  • JSON objects use similar syntax to Dart maps.
  • The dart:convert library allows you to convert between JSON and Dart maps.

Where to Go From Here?

As you continue to create Dart applications, you’ll find that you’re writing methods and constructors like toJson and fromJson repeatedly. If you find that tiring, consider using a code generation package from pub.dev, such as freezed or json_serializable. You only need to write the properties for the class, and these packages will generate the rest.

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now