Dart MapMixin
last modified April 4, 2025
MapMixin is a mixin class in Dart that provides default implementations of Map methods. It allows you to create custom map-like classes with minimal effort.
By mixing in MapMixin, you only need to implement a few core methods while getting all other Map operations for free. This follows the DRY principle.
Basic MapMixin Implementation
Here's the simplest way to create a custom map class using MapMixin.
import 'dart:collection'; class SimpleMap<K, V> with MapMixin<K, V> { final _storage = <K, V>{}; @override V? operator [](Object? key) => _storage[key]; @override void operator []=(K key, V value) => _storage[key] = value; @override void clear() => _storage.clear(); @override Iterable<K> get keys => _storage.keys; @override V? remove(Object? key) => _storage.remove(key); } void main() { var map = SimpleMap<String, int>(); map['one'] = 1; map['two'] = 2; print(map); print(map['one']); }
We create SimpleMap by mixing in MapMixin. We only implement 5 required methods and get all other Map operations automatically. The _storage field holds data.
$ dart main.dart {one: 1, two: 2} 1
Custom Map with Validation
We can extend MapMixin to create a map with custom validation logic.
import 'dart:collection'; class ValidatingMap<K, V> with MapMixin<K, V> { final _data = <K, V>{}; final bool Function(K key, V value) validator; ValidatingMap(this.validator); @override V? operator [](Object? key) => _data[key]; @override void operator []=(K key, V value) { if (validator(key, value)) { _data[key] = value; } else { throw ArgumentError('Invalid entry: $key=$value'); } } @override void clear() => _data.clear(); @override Iterable<K> get keys => _data.keys; @override V? remove(Object? key) => _data.remove(key); } void main() { var positiveMap = ValidatingMap<String, int>( (key, value) => value > 0); positiveMap['valid'] = 42; print(positiveMap); try { positiveMap['invalid'] = -1; } catch (e) { print('Error: $e'); } }
This ValidatingMap only accepts entries that pass the validator function. Here we ensure all values are positive integers. The validator runs before insertion.
$ dart main.dart {valid: 42} Error: ArgumentError: Invalid entry: invalid=-1
Case Insensitive String Map
MapMixin can help create maps with custom key comparison logic.
import 'dart:collection'; class CaseInsensitiveMap<V> with MapMixin<String, V> { final _data = <String, V>{}; @override V? operator [](Object? key) => _data[_normalizeKey(key)]; @override void operator []=(String key, V value) => _data[_normalizeKey(key)] = value; @override void clear() => _data.clear(); @override Iterable<String> get keys => _data.keys; @override V? remove(Object? key) => _data.remove(_normalizeKey(key)); String _normalizeKey(Object? key) => key?.toString().toLowerCase() ?? ''; } void main() { var ciMap = CaseInsensitiveMap<int>(); ciMap['Hello'] = 1; ciMap['HELLO'] = 2; ciMap['hello'] = 3; print(ciMap); print(ciMap['hElLo']); }
This map treats keys case-insensitively by normalizing them to lowercase. All key operations use the normalized version, making lookups case-insensitive.
$ dart main.dart {hello: 3} 3
Counting Map with MapMixin
We can create a map that counts operations using MapMixin.
import 'dart:collection'; class CountingMap<K, V> with MapMixin<K, V> { final _data = <K, V>{}; int _accessCount = 0; int _modifyCount = 0; int get accessCount => _accessCount; int get modifyCount => _modifyCount; @override V? operator [](Object? key) { _accessCount++; return _data[key]; } @override void operator []=(K key, V value) { _modifyCount++; _data[key] = value; } @override void clear() { _modifyCount++; _data.clear(); } @override Iterable<K> get keys => _data.keys; @override V? remove(Object? key) { _modifyCount++; return _data.remove(key); } } void main() { var counter = CountingMap<String, int>(); counter['a'] = 1; counter['b'] = 2; print(counter['a']); counter.remove('b'); print('Accesses: ${counter.accessCount}'); print('Modifies: ${counter.modifyCount}'); }
This CountingMap tracks how many times it's accessed or modified. We override operations to increment counters while delegating storage to the _data map.
$ dart main.dart 1 Accesses: 1 Modifies: 3
LRU Cache with MapMixin
MapMixin can help implement more complex structures like an LRU cache.
import 'dart:collection'; class LRUCache<K, V> with MapMixin<K, V> { final LinkedHashMap<K, V> _storage; final int maxSize; LRUCache(this.maxSize) : _storage = LinkedHashMap<K, V>(); @override V? operator [](Object? key) { if (_storage.containsKey(key)) { var value = _storage.remove(key); _storage[key as K] = value!; return value; } return null; } @override void operator []=(K key, V value) { if (_storage.length >= maxSize && !_storage.containsKey(key)) { _storage.remove(_storage.keys.first); } _storage.remove(key); _storage[key] = value; } @override void clear() => _storage.clear(); @override Iterable<K> get keys => _storage.keys; @override V? remove(Object? key) => _storage.remove(key); } void main() { var cache = LRUCache<String, int>(3); cache['a'] = 1; cache['b'] = 2; cache['c'] = 3; print(cache); // {a: 1, b: 2, c: 3} // Access 'a' to make it most recently used print(cache['a']); // Adding new item evicts least recently used ('b') cache['d'] = 4; print(cache); // {c: 3, a: 1, d: 4} }
This LRU cache maintains access order using LinkedHashMap. When capacity is reached, the least recently used item is evicted. Accessing items updates their position.
$ dart main.dart {a: 1, b: 2, c: 3} 1 {c: 3, a: 1, d: 4}
Best Practices
- Minimal Implementation: Only implement required methods when using MapMixin.
- Consistent Behavior: Ensure your custom map follows Map interface contracts.
- Performance: Consider performance of your core operations.
- Documentation: Clearly document any special behavior.
Source
This tutorial covered Dart's MapMixin with practical examples demonstrating how to create custom map implementations efficiently.
Author
List all Dart tutorials.