ZetCode

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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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.

main.dart
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

Source

Dart MapMixin Documentation

This tutorial covered Dart's MapMixin with practical examples demonstrating how to create custom map implementations efficiently.

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.