ZetCode

Dart UnmodifiableMapMixin

last modified April 4, 2025

The UnmodifiableMapMixin in Dart provides a way to create immutable map views. It prevents modifications to the underlying map by throwing exceptions.

This mixin implements all mutating operations to throw UnsupportedError. It's used by UnmodifiableMapView and other immutable map implementations.

Basic Usage of UnmodifiableMapMixin

Here's how to create a simple class that uses UnmodifiableMapMixin.

main.dart
import 'dart:collection';

class ImmutableMap<K, V> with UnmodifiableMapMixin<K, V> {
  final Map<K, V> _map;

  ImmutableMap(this._map);

  @override
  V? operator [](Object? key) => _map[key];

  @override
  Iterable<K> get keys => _map.keys;
}

void main() {
  var data = {'a': 1, 'b': 2, 'c': 3};
  var immutable = ImmutableMap(data);

  print(immutable['a']); // 1
  
  try {
    immutable['d'] = 4; // Throws
  } catch (e) {
    print('Error: $e');
  }
}

We create an ImmutableMap class that wraps a regular Map. The mixin prevents modifications while allowing read operations. Attempts to modify throw errors.

$ dart main.dart
1
Error: Unsupported operation: Cannot modify unmodifiable map

Creating UnmodifiableMapView

Dart provides UnmodifiableMapView which uses UnmodifiableMapMixin internally.

main.dart
import 'dart:collection';

void main() {
  var original = {'name': 'Alice', 'age': 30};
  var unmodifiable = UnmodifiableMapView(original);

  print(unmodifiable['name']); // Alice
  
  try {
    unmodifiable['age'] = 31; // Throws
  } catch (e) {
    print('Error: $e');
  }
  
  // Changes to original reflect in view
  original['age'] = 31;
  print(unmodifiable['age']); // 31
}

UnmodifiableMapView provides a read-only view of the original map. Note that changes to the original map are visible in the view, as it's just a wrapper.

$ dart main.dart
Alice
Error: Unsupported operation: Cannot modify unmodifiable map
31

Implementing Custom Unmodifiable Map

Here's a more complete custom implementation using UnmodifiableMapMixin.

main.dart
import 'dart:collection';

class SafeMap<K, V> with UnmodifiableMapMixin<K, V> {
  final Map<K, V> _source;

  SafeMap(Map<K, V> map) : _source = Map.of(map);

  @override
  V? operator [](Object? key) => _source[key];

  @override
  Iterable<K> get keys => _source.keys;

  @override
  int get length => _source.length;

  @override
  bool containsKey(Object? key) => _source.containsKey(key);

  @override
  bool containsValue(Object? value) => _source.containsValue(value);
}

void main() {
  var data = {'x': 10, 'y': 20};
  var safeMap = SafeMap(data);

  print(safeMap['x']); // 10
  print(safeMap.containsKey('y')); // true
  
  try {
    safeMap.clear(); // Throws
  } catch (e) {
    print('Error: $e');
  }
}

This SafeMap class provides a complete immutable map implementation. It copies the source map to prevent external modifications from affecting the immutable view.

$ dart main.dart
10
true
Error: Unsupported operation: Cannot modify unmodifiable map

Combining with Other Mixins

UnmodifiableMapMixin can be combined with other mixins for enhanced functionality.

main.dart
import 'dart:collection';

class ReportingMap<K, V> with UnmodifiableMapMixin<K, V> {
  final Map<K, V> _map;
  int _accessCount = 0;

  ReportingMap(this._map);

  @override
  V? operator [](Object? key) {
    _accessCount++;
    print('Access #$_accessCount to key $key');
    return _map[key];
  }

  @override
  Iterable<K> get keys => _map.keys;
}

void main() {
  var map = ReportingMap({'a': 1, 'b': 2});
  
  print(map['a']);
  print(map['b']);
  print(map['a']);
  
  try {
    map['c'] = 3; // Throws
  } catch (e) {
    print('Error: $e');
  }
}

This ReportingMap tracks access counts while remaining unmodifiable. It shows how to add functionality while maintaining immutability via the mixin.

$ dart main.dart
Access #1 to key a
1
Access #2 to key b
2
Access #3 to key a
1
Error: Unsupported operation: Cannot modify unmodifiable map

Testing Unmodifiable Behavior

Here's how to verify all modification methods throw exceptions.

main.dart
import 'dart:collection';

void testUnmodifiable(Map<String, int> map) {
  final methods = [
    () => map['x'] = 1,
    () => map.clear(),
    () => map.remove('x'),
    () => map.putIfAbsent('x', () => 1),
    () => map.update('x', (v) => v + 1),
  ];

  for (var method in methods) {
    try {
      method();
      print('Method did not throw');
    } catch (e) {
      print('Caught: ${e.runtimeType}');
    }
  }
}

void main() {
  var unmodifiable = UnmodifiableMapView({'a': 1});
  testUnmodifiable(unmodifiable);
}

This test verifies that all mutating operations throw exceptions. The UnmodifiableMapMixin ensures consistent behavior across all methods.

$ dart main.dart
Caught: UnsupportedError
Caught: UnsupportedError
Caught: UnsupportedError
Caught: UnsupportedError
Caught: UnsupportedError

Best Practices

Source

Dart UnmodifiableMapMixin Documentation

This tutorial covered Dart's UnmodifiableMapMixin with practical examples demonstrating immutable map creation and usage patterns.

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.