Dart MapBase
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.
Basic MapBase Implementation
Here's how to create a simple map by extending MapBase.
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.
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.
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.
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.
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).
Best Practices
- 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.
Source
This tutorial covered Dart's MapBase
with practical examples
demonstrating how to create custom map implementations by extending this
abstract base class.
Author
List all Dart tutorials.