Dart MapBase tutorial shows how to work with the abstract base class for maps in Dart.
last modified June 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.
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)
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
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
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
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> _map = {}; final List<void Function(K, V?)> _listeners = [];
void addListener(void Function(K, V?) listener) { _listeners.add(listener); }
@override V? operator [](Object? key) => _map[key as K];
@override void operator []=(K key, V value) { _map[key] = value; _notifyListeners(key, value); }
@override void clear() { for (var key in _map.keys.toList()) { _notifyListeners(key, null); // Notify before clearing } _map.clear(); }
@override Iterable<K> get keys => _map.keys;
@override V? remove(Object? key) { if (!_map.containsKey(key)) return null; var value = _map.remove(key); _notifyListeners(key as K, null); return value; }
void _notifyListeners(K key, V? value) { for (var listener in _listeners) { listener(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’); map.clear(); // Will correctly notify all removals }
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).
Minimal Implementation: Only implement required methods when extending MapBase.
Consistent Equality: Ensure key equality is consistent with hashCode.
Null Safety: Handle null keys/values appropriately in your implementation.
Performance: Consider performance characteristics of your storage backend.
This tutorial covered Dart’s MapBase with practical examples demonstrating how to create custom map implementations by extending this abstract base class.
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.