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.
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>{}; 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
- 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.