ZetCode

Dart MapBase

last modified April 4, 2025

MapBase is an abstract base class for maps in Dart. It provides common functionality shared by all map implementations.

This class implements most Map operations but leaves the actual storage implementation to subclasses. It's useful when creating custom map types.

Basic MapBase Implementation

Here's how to create a simple map by extending MapBase.

main.dart
import 'dart:collection';

class SimpleMap<K, V> extends MapBase<K, V> {
  final _map = <K, V>{};

  @override
  V? operator [](Object? key) => _map[key];

  @override
  void operator []=(K key, V value) => _map[key] = value;

  @override
  void clear() => _map.clear();

  @override
  Iterable<K> get keys => _map.keys;

  @override
  V? remove(Object? key) => _map.remove(key);
}

void main() {
  var map = SimpleMap<String, int>();
  map['one'] = 1;
  map['two'] = 2;
  
  print(map['one']); // 1
  print(map.keys);   // (one, two)
}

We implement all required abstract methods from MapBase. The actual storage is delegated to a private Map. This pattern is common for custom map types.

$ dart main.dart
1
(one, two)

Counting Map Implementation

We can create a specialized map that counts value occurrences.

main.dart
import 'dart:collection';

class CountingMap<K> extends MapBase<K, int> {
  final _counts = <K, int>{};

  void increment(K key) {
    _counts[key] = (_counts[key] ?? 0) + 1;
  }

  @override
  int? operator [](Object? key) => _counts[key];

  @override
  void operator []=(K key, int value) => _counts[key] = value;

  @override
  void clear() => _counts.clear();

  @override
  Iterable<K> get keys => _counts.keys;

  @override
  int? remove(Object? key) => _counts.remove(key);
}

void main() {
  var counter = CountingMap<String>();
  counter.increment('apple');
  counter.increment('apple');
  counter.increment('banana');
  
  print(counter['apple']);  // 2
  print(counter['banana']); // 1
}

This CountingMap adds a custom increment method while still behaving like a regular map. It demonstrates how to extend MapBase with domain-specific logic.

$ dart main.dart
2
1

Case Insensitive Map

Here's a map implementation that treats keys case-insensitively.

main.dart
import 'dart:collection';

class CaseInsensitiveMap<V> extends MapBase<String, V> {
  final _map = <String, V>{};

  String _normalizeKey(String key) => key.toLowerCase();

  @override
  V? operator [](Object? key) => _map[_normalizeKey(key as String)];

  @override
  void operator []=(String key, V value) => _map[_normalizeKey(key)] = value;

  @override
  void clear() => _map.clear();

  @override
  Iterable<String> get keys => _map.keys;

  @override
  V? remove(Object? key) => _map.remove(_normalizeKey(key as String));
}

void main() {
  var map = CaseInsensitiveMap<int>();
  map['Hello'] = 1;
  map['HELLO'] = 2;
  map['hello'] = 3;
  
  print(map.length);    // 1
  print(map['HeLlO']);  // 3
}

The map normalizes all keys to lowercase before storage. Different case variations of the same string map to the same entry.

$ dart main.dart
1
3

Default Value Map

This map returns a default value when a key is not found.

main.dart
import 'dart:collection';

class DefaultValueMap<K, V> extends MapBase<K, V> {
  final _map = <K, V>{};
  final V defaultValue;

  DefaultValueMap(this.defaultValue);

  @override
  V operator [](Object? key) => _map[key] ?? defaultValue;

  @override
  void operator []=(K key, V value) => _map[key] = value;

  @override
  void clear() => _map.clear();

  @override
  Iterable<K> get keys => _map.keys;

  @override
  V? remove(Object? key) => _map.remove(key);
}

void main() {
  var map = DefaultValueMap<String, int>(0);
  map['a'] = 1;
  
  print(map['a']); // 1
  print(map['b']); // 0
}

The map is initialized with a default value. When accessing non-existent keys, it returns this default instead of null. This is useful for counting patterns.

$ dart main.dart
1
0

Observable Map

We can create a map that notifies listeners of changes.

main.dart
import 'dart:collection';

class ObservableMap<K, V> extends MapBase<K, V> {
  final _map = <K, V>{};
  final List<Function(K, V?)> _listeners = [];

  void addListener(Function(K, V?) listener) {
    _listeners.add(listener);
  }

  @override
  V? operator [](Object? key) => _map[key];

  @override
  void operator []=(K key, V value) {
    _map[key] = value;
    _notifyListeners(key, value);
  }

  @override
  void clear() {
    _map.clear();
    _listeners.forEach((fn) => fn(null, null));
  }

  @override
  Iterable<K> get keys => _map.keys;

  @override
  V? remove(Object? key) {
    var value = _map.remove(key);
    if (value != null) _notifyListeners(key as K, null);
    return value;
  }

  void _notifyListeners(K key, V? value) {
    _listeners.forEach((fn) => fn(key, value));
  }
}

void main() {
  var map = ObservableMap<String, int>();
  map.addListener((key, value) {
    print('Change: $key = $value');
  });
  
  map['a'] = 1;
  map['b'] = 2;
  map.remove('a');
}

This map maintains a list of listeners and notifies them of changes. The notification includes the changed key and its new value (or null if removed).

$ dart main.dart
Change: a = 1
Change: b = 2
Change: a = null

Best Practices

Source

Dart MapBase Documentation

This tutorial covered Dart's MapBase with practical examples demonstrating how to create custom map implementations by extending this abstract base class.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.