Dart Programming Fundamentals

Collections: Maps & Sets

40 min Lesson 10 of 13

Introduction to Maps and Sets

In the previous lesson we explored Lists, Dart’s ordered collection. Now we turn to two equally important collection types: Maps and Sets. Maps store data as key-value pairs, perfect for lookups and dictionaries. Sets store unique, unordered values, ideal for membership tests and eliminating duplicates. Together with Lists, these three types cover virtually every data-structure need in Dart.

Maps: Key-Value Pairs

A Map associates keys with values. Each key is unique and maps to exactly one value. Think of it like a real dictionary: you look up a word (the key) and get its definition (the value).

Creating Maps

There are several ways to create a Map in Dart:

Map Literal Syntax

void main() {
  // Type-annotated map literal
  Map<String, int> ages = {
    'Ahmed': 25,
    'Sara': 30,
    'Ali': 22,
  };

  // Type-inferred map literal
  var capitals = {
    'Saudi Arabia': 'Riyadh',
    'Egypt': 'Cairo',
    'Jordan': 'Amman',
  };

  // Empty map with explicit types
  Map<String, double> prices = {};

  // Empty map using constructor
  var scores = Map<String, int>();

  print(ages);      // {Ahmed: 25, Sara: 30, Ali: 22}
  print(capitals);  // {Saudi Arabia: Riyadh, Egypt: Cairo, Jordan: Amman}
}
Note: When you create an empty map with var emptyMap = {};, Dart infers the type as Map<dynamic, dynamic>. Always specify the type explicitly for empty maps: Map<String, int> emptyMap = {}; or var emptyMap = <String, int>{};.

Map<K, V> Type Syntax

The generic type Map<K, V> specifies the key type K and value type V. This ensures type safety at compile time.

Typed Maps

Map<String, int> wordCount = {};       // String keys, int values
Map<int, String> idToName = {};         // int keys, String values
Map<String, List<String>> groups = {};  // String keys, List values
Map<String, Map<String, int>> nested = {}; // Nested maps

Accessing and Modifying Map Entries

You use bracket notation to read, add, update, and remove entries in a map:

Accessing Map Values

void main() {
  var user = {
    'name': 'Edrees',
    'email': 'edrees@example.com',
    'age': '28',
  };

  // Reading a value
  print(user['name']);   // Edrees
  print(user['email']);  // edrees@example.com

  // Accessing a non-existent key returns null
  print(user['phone']);  // null

  // Adding a new entry
  user['phone'] = '+966555555555';
  print(user['phone']);  // +966555555555

  // Updating an existing entry
  user['age'] = '29';
  print(user['age']);    // 29

  // Removing an entry
  user.remove('email');
  print(user);  // {name: Edrees, age: 29, phone: +966555555555}
}
Warning: Accessing a key that does not exist returns null, not an error. This means the return type of map[key] is always nullable (V?). Always check for null or use the ! operator only when you are certain the key exists.

Essential Map Properties

Map Properties

void main() {
  var fruits = {
    'apple': 3,
    'banana': 5,
    'orange': 2,
  };

  print(fruits.length);    // 3
  print(fruits.isEmpty);   // false
  print(fruits.isNotEmpty); // true
  print(fruits.keys);      // (apple, banana, orange)
  print(fruits.values);    // (3, 5, 2)
  print(fruits.entries);   // (MapEntry(apple: 3), MapEntry(banana: 5), MapEntry(orange: 2))
}

Important Map Methods

Dart’s Map class provides a rich set of methods for querying and transforming data:

containsKey and containsValue

void main() {
  var inventory = {'laptop': 10, 'phone': 25, 'tablet': 8};

  print(inventory.containsKey('laptop'));   // true
  print(inventory.containsKey('desktop'));  // false
  print(inventory.containsValue(25));        // true
  print(inventory.containsValue(100));       // false
}

putIfAbsent

void main() {
  var settings = {'theme': 'dark', 'language': 'en'};

  // Only adds if the key does not already exist
  settings.putIfAbsent('theme', () => 'light');
  print(settings['theme']);  // dark (unchanged, key already existed)

  settings.putIfAbsent('fontSize', () => '14');
  print(settings['fontSize']);  // 14 (added because key was absent)

  print(settings);  // {theme: dark, language: en, fontSize: 14}
}

update Method

void main() {
  var stock = {'apples': 10, 'bananas': 5};

  // Update an existing key
  stock.update('apples', (value) => value + 5);
  print(stock['apples']);  // 15

  // Update with ifAbsent -- adds the key if it does not exist
  stock.update('grapes', (value) => value + 1, ifAbsent: () => 20);
  print(stock['grapes']);  // 20

  print(stock);  // {apples: 15, bananas: 5, grapes: 20}
}

forEach on Maps

void main() {
  var prices = {'Coffee': 15.0, 'Tea': 10.0, 'Juice': 12.5};

  prices.forEach((key, value) {
    print('$key costs $value SAR');
  });
  // Coffee costs 15.0 SAR
  // Tea costs 10.0 SAR
  // Juice costs 12.5 SAR
}

map Method -- Transforming a Map

void main() {
  var prices = {'Coffee': 15.0, 'Tea': 10.0, 'Juice': 12.5};

  // Apply 15% VAT to all prices
  var withVat = prices.map((key, value) {
    return MapEntry(key, value * 1.15);
  });

  print(withVat);
  // {Coffee: 17.25, Tea: 11.5, Juice: 14.375}
}

Other Useful Map Methods

void main() {
  var scores = {'Ahmed': 85, 'Sara': 92, 'Ali': 78, 'Fatima': 95};

  // addAll -- merge another map into this one
  scores.addAll({'Omar': 88, 'Noor': 91});
  print(scores.length);  // 6

  // removeWhere -- remove entries matching a condition
  scores.removeWhere((key, value) => value < 80);
  print(scores);  // {Ahmed: 85, Sara: 92, Fatima: 95, Omar: 88, Noor: 91}

  // clear -- remove all entries
  scores.clear();
  print(scores);  // {}
}

Iterating Over Maps

There are several ways to loop through a map:

Different Iteration Approaches

void main() {
  var countries = {'SA': 'Saudi Arabia', 'EG': 'Egypt', 'JO': 'Jordan'};

  // 1. forEach method
  countries.forEach((code, name) {
    print('$code => $name');
  });

  // 2. for-in with entries
  for (var entry in countries.entries) {
    print('${entry.key} => ${entry.value}');
  }

  // 3. Iterate over keys only
  for (var code in countries.keys) {
    print('Code: $code');
  }

  // 4. Iterate over values only
  for (var name in countries.values) {
    print('Country: $name');
  }
}

Nested Maps

Maps can contain other maps as values, which is common when working with structured data like JSON:

Nested Maps Example

void main() {
  Map<String, Map<String, dynamic>> users = {
    'user1': {
      'name': 'Ahmed',
      'age': 25,
      'address': {
        'city': 'Riyadh',
        'country': 'Saudi Arabia',
      },
    },
    'user2': {
      'name': 'Sara',
      'age': 30,
      'address': {
        'city': 'Cairo',
        'country': 'Egypt',
      },
    },
  };

  // Accessing nested values
  print(users['user1']!['name']);  // Ahmed
  var address = users['user1']!['address'] as Map;
  print(address['city']);  // Riyadh
}
Pro Tip: When working with deeply nested maps (e.g., JSON data), consider creating Dart classes instead. Classes provide type safety, autocompletion, and are much easier to maintain than deeply nested Map<String, dynamic> structures.

Sets: Unique Unordered Collections

A Set is a collection of unique values with no guaranteed order. Unlike Lists, a Set never contains duplicate elements. This makes Sets perfect for tracking unique items, membership tests, and mathematical set operations.

Creating Sets

Set Creation

void main() {
  // Set literal with type annotation
  Set<String> colors = {'red', 'green', 'blue'};

  // Type-inferred set
  var numbers = {1, 2, 3, 4, 5};

  // Empty set (must specify type or use Set constructor)
  Set<int> emptySet = {};
  var anotherEmpty = <String>{};

  // Set from a list (removes duplicates)
  var listWithDupes = [1, 2, 2, 3, 3, 3, 4];
  var uniqueNumbers = listWithDupes.toSet();
  print(uniqueNumbers);  // {1, 2, 3, 4}

  // Set.from constructor
  var fromList = Set<int>.from([10, 20, 20, 30]);
  print(fromList);  // {10, 20, 30}
}
Warning: Be careful with empty set vs empty map syntax. var x = {}; creates an empty Map, not a Set. To create an empty Set, use var x = <Type>{}; or Set<Type> x = {};.

Adding and Removing Elements

Set Modifications

void main() {
  var tags = <String>{'dart', 'flutter'};

  // add -- returns true if element was added (new)
  bool added = tags.add('firebase');
  print(added);  // true
  print(tags);   // {dart, flutter, firebase}

  // Adding a duplicate does nothing
  added = tags.add('dart');
  print(added);  // false (already exists)
  print(tags);   // {dart, flutter, firebase}

  // addAll -- add multiple elements
  tags.addAll({'android', 'ios', 'dart'});
  print(tags);  // {dart, flutter, firebase, android, ios}

  // remove -- returns true if element was removed
  tags.remove('android');
  print(tags);  // {dart, flutter, firebase, ios}

  // removeWhere
  tags.removeWhere((tag) => tag.length > 5);
  print(tags);  // {dart, ios}
}

Set Membership and Properties

Checking Membership

void main() {
  var permissions = {'read', 'write', 'execute'};

  print(permissions.contains('read'));    // true
  print(permissions.contains('delete'));  // false
  print(permissions.length);              // 3
  print(permissions.isEmpty);             // false
  print(permissions.isNotEmpty);          // true

  // containsAll -- check if all elements exist
  print(permissions.containsAll({'read', 'write'}));  // true
  print(permissions.containsAll({'read', 'admin'}));  // false
}
Pro Tip: The contains() method on a Set runs in O(1) constant time, while on a List it runs in O(n) linear time. If you frequently check whether a value exists in a large collection, convert it to a Set for much better performance.

Set Operations: Union, Intersection, and Difference

Sets support the classic mathematical set operations, which are incredibly useful for comparing and combining data:

Set Operations

void main() {
  var frontend = {'HTML', 'CSS', 'JavaScript', 'Dart'};
  var backend = {'PHP', 'Python', 'Dart', 'JavaScript'};

  // Union -- all elements from both sets (no duplicates)
  var allLanguages = frontend.union(backend);
  print(allLanguages);
  // {HTML, CSS, JavaScript, Dart, PHP, Python}

  // Intersection -- elements in BOTH sets
  var shared = frontend.intersection(backend);
  print(shared);  // {JavaScript, Dart}

  // Difference -- elements in first set but NOT in second
  var frontendOnly = frontend.difference(backend);
  print(frontendOnly);  // {HTML, CSS}

  var backendOnly = backend.difference(frontend);
  print(backendOnly);  // {PHP, Python}
}

Removing Duplicates from a List

One of the most common uses of Sets is removing duplicates from a List:

Deduplication Pattern

void main() {
  var votes = ['red', 'blue', 'red', 'green', 'blue', 'red', 'green'];

  // Convert to Set to remove duplicates, then back to List
  var uniqueVotes = votes.toSet().toList();
  print(uniqueVotes);  // [red, blue, green]

  // Count unique voters
  var voterIds = [101, 102, 101, 103, 102, 104];
  var uniqueVoters = voterIds.toSet();
  print('Total votes: ${voterIds.length}');       // 6
  print('Unique voters: ${uniqueVoters.length}');  // 4
}

Practical Examples

Word Counter using Maps

Word Frequency Counter

void main() {
  var text = 'the cat sat on the mat the cat';
  var words = text.split(' ');

  Map<String, int> wordCount = {};
  for (var word in words) {
    wordCount.update(word, (count) => count + 1, ifAbsent: () => 1);
  }

  print(wordCount);
  // {the: 3, cat: 2, sat: 1, on: 1, mat: 1}

  // Find the most frequent word
  var mostFrequent = wordCount.entries
      .reduce((a, b) => a.value > b.value ? a : b);
  print('Most frequent: "${mostFrequent.key}" (${mostFrequent.value} times)');
  // Most frequent: "the" (3 times)
}

Configuration Storage using Maps

App Configuration

void main() {
  Map<String, dynamic> config = {
    'appName': 'My App',
    'version': '2.1.0',
    'maxRetries': 3,
    'debugMode': false,
    'supportedLocales': ['en', 'ar', 'fr'],
    'api': {
      'baseUrl': 'https://api.example.com',
      'timeout': 30,
    },
  };

  // Safe access with null check
  var appName = config['appName'] ?? 'Unknown';
  print(appName);  // My App

  // Update a setting
  config['debugMode'] = true;

  // Add a new setting only if it does not exist
  config.putIfAbsent('cacheEnabled', () => true);
  print(config['cacheEnabled']);  // true
}

Tracking Unique Items with Sets

Unique Page Visitors

void main() {
  // Simulating page visits (some users visit multiple times)
  var pageVisits = [
    {'userId': 'u1', 'page': '/home'},
    {'userId': 'u2', 'page': '/home'},
    {'userId': 'u1', 'page': '/about'},
    {'userId': 'u3', 'page': '/home'},
    {'userId': 'u1', 'page': '/home'},
    {'userId': 'u2', 'page': '/about'},
  ];

  // Unique visitors per page
  Map<String, Set<String>> uniqueVisitors = {};
  for (var visit in pageVisits) {
    var page = visit['page']!;
    var user = visit['userId']!;
    uniqueVisitors.putIfAbsent(page, () => <String>{});
    uniqueVisitors[page]!.add(user);
  }

  uniqueVisitors.forEach((page, visitors) {
    print('$page: ${visitors.length} unique visitors $visitors');
  });
  // /home: 3 unique visitors {u1, u2, u3}
  // /about: 2 unique visitors {u1, u2}
}

When to Use Map vs Set vs List

Choosing the right collection type is an important design decision:

  • List -- Use when order matters, duplicates are allowed, and you access elements by index. Example: a list of chat messages, an ordered queue of tasks.
  • Map -- Use when you need to look up values by a unique key. Example: user profiles by ID, app configuration, word frequency counts.
  • Set -- Use when you need unique values with fast membership checks. Example: tracking which items a user has seen, collecting unique tags, permissions.
Note: You can combine these types freely. A Map<String, Set<String>> maps keys to sets of unique values. A List<Map<String, dynamic>> is essentially a list of records (similar to a database result set). Choose the combination that best models your data.

Summary

  • Maps store key-value pairs with Map<K, V> syntax
  • Access map values with bracket notation: map[key]
  • Key map methods: containsKey, containsValue, putIfAbsent, update, forEach, map, addAll, removeWhere
  • Sets store unique values with Set<T> syntax
  • Key set methods: add, remove, contains, union, intersection, difference
  • Convert a List to a Set with .toSet() to remove duplicates
  • Use Maps for lookups, Sets for uniqueness, Lists for ordered sequences

Practice Exercise

Open DartPad and build a small inventory management program. Create a Map<String, int> for product stock (e.g., 'laptop': 10, 'phone': 25). Write functions to: (1) add stock using update with ifAbsent, (2) remove a product, (3) list all products with stock below 5. Then create a Set<String> of categories. Add categories, check membership, and compute the union of two category sets. Print the results of each operation.