ZetCode

Dart UnmodifiableMapBase

last modified April 4, 2025

In Dart, UnmodifiableMapBase is an abstract base class for creating unmodifiable map views. It provides read-only access to map data while preventing modifications.

UnmodifiableMapBase implements the Map interface but throws UnsupportedError for all mutating operations. It's useful for exposing map data safely without allowing changes.

Basic UnmodifiableMapBase Usage

Here's how to create a simple unmodifiable map by extending UnmodifiableMapBase.

main.dart
import 'dart:collection';

class UnmodifiableMapView<K, V> extends UnmodifiableMapBase<K, V> {
  final Map<K, V> _source;

  UnmodifiableMapView(this._source);

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

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

void main() {
  final source = {'a': 1, 'b': 2, 'c': 3};
  final unmodifiable = UnmodifiableMapView(source);

  print(unmodifiable['a']); // 1
  print(unmodifiable.length); // 3

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

We create an UnmodifiableMapView class that wraps a source map. The [] operator and keys getter must be implemented. Attempts to modify throw exceptions.

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

Creating an Unmodifiable Map with Factory

A more practical approach uses a factory method to create unmodifiable maps.

main.dart
import 'dart:collection';

class UnmodifiableMapFactory {
  static Map<K, V> create<K, V>(Map<K, V> source) {
    return _UnmodifiableMapWrapper(source);
  }
}

class _UnmodifiableMapWrapper<K, V> extends UnmodifiableMapBase<K, V> {
  final Map<K, V> _source;

  _UnmodifiableMapWrapper(this._source);

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

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

void main() {
  final source = {'x': 10, 'y': 20, 'z': 30};
  final unmodifiable = UnmodifiableMapFactory.create(source);

  print(unmodifiable['y']); // 20
  print(unmodifiable.containsKey('x')); // true

  try {
    unmodifiable.clear(); // Throws
  } catch (e) {
    print('Error: $e');
  }
}

This example shows a factory pattern for creating unmodifiable maps. The private _UnmodifiableMapWrapper class implements the unmodifiable behavior.

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

Unmodifiable Map with Custom Key-Value Types

UnmodifiableMapBase works with custom types while maintaining immutability.

main.dart
import 'dart:collection';

class Product {
  final String id;
  final String name;
  
  Product(this.id, this.name);
  
  @override
  String toString() => 'Product($id, $name)';
}

class UnmodifiableProductMap extends UnmodifiableMapBase<String, Product> {
  final Map<String, Product> _products;
  
  UnmodifiableProductMap(this._products);
  
  @override
  Product? operator [](Object? key) => _products[key];
  
  @override
  Iterable<String> get keys => _products.keys;
}

void main() {
  final products = {
    'p1': Product('p1', 'Laptop'),
    'p2': Product('p2', 'Phone'),
    'p3': Product('p3', 'Tablet')
  };
  
  final unmodifiable = UnmodifiableProductMap(products);
  
  print(unmodifiable['p2']); // Product(p2, Phone)
  print(unmodifiable.values.where((p) => p.name.length > 5).toList());
  
  try {
    unmodifiable['p4'] = Product('p4', 'Monitor'); // Throws
  } catch (e) {
    print('Modification error: $e');
  }
}

We create a type-safe unmodifiable map for Product objects. The map provides read-only access while preventing any modifications to the product collection.

$ dart main.dart
Product(p2, Phone)
[Product(p1, Laptop), Product(p3, Tablet)]
Modification error: Unsupported operation: Cannot modify unmodifiable map

Combining UnmodifiableMapBase with Other Collections

UnmodifiableMapBase can be combined with other collection types for complex immutable data structures.

main.dart
import 'dart:collection';

class ImmutableConfig extends UnmodifiableMapBase<String, dynamic> {
  final Map<String, dynamic> _config;
  final List<String> _log;
  
  ImmutableConfig(this._config, this._log);
  
  @override
  dynamic operator [](Object? key) {
    _log.add('Accessed: $key');
    return _config[key];
  }
  
  @override
  Iterable<String> get keys => _config.keys;
}

void main() {
  final config = {
    'timeout': 30,
    'retries': 3,
    'debug': false
  };
  
  final accessLog = <String>[];
  final immutableConfig = ImmutableConfig(config, accessLog);
  
  print(immutableConfig['timeout']); // 30
  print(immutableConfig['debug']);   // false
  
  try {
    immutableConfig['timeout'] = 60; // Throws
  } catch (e) {
    print('Error: $e');
  }
  
  print('Access log: $accessLog');
}

This example combines an unmodifiable map with a logging list. The map remains immutable while tracking access attempts. The log list can be accessed separately.

$ dart main.dart
30
false
Error: Unsupported operation: Cannot modify unmodifiable map
Access log: [Accessed: timeout, Accessed: debug]

Advanced UnmodifiableMapBase with Lazy Loading

We can implement lazy loading in an unmodifiable map by overriding the accessors.

main.dart
import 'dart:collection';

class LazyUnmodifiableMap extends UnmodifiableMapBase<String, String> {
  final Map<String, String> _source;
  final Map<String, String> _cache = {};
  
  LazyUnmodifiableMap(this._source);
  
  @override
  String? operator [](Object? key) {
    if (key is! String) return null;
    
    if (!_cache.containsKey(key) {
      print('Loading $key from source...');
      _cache[key] = _source[key] ?? 'DEFAULT';
    }
    
    return _cache[key];
  }
  
  @override
  Iterable<String> get keys => _source.keys;
}

void main() {
  final source = {
    'config1': 'Value1',
    'config2': 'Value2',
    'config3': 'Value3'
  };
  
  final lazyMap = LazyUnmodifiableMap(source);
  
  print('First access:');
  print(lazyMap['config2']); // Loads and caches
  
  print('\nSecond access:');
  print(lazyMap['config2']); // Uses cache
  
  print('\nAll keys:');
  print(lazyMap.keys.join(', '));
  
  try {
    lazyMap['new'] = 'value'; // Throws
  } catch (e) {
    print('\nError: $e');
  }
}

This advanced example implements lazy loading with caching. Values are loaded on first access and cached for subsequent requests. The map remains unmodifiable.

$ dart main.dart
First access:
Loading config2 from source...
Value2

Second access:
Value2

All keys:
config1, config2, config3

Error: Unsupported operation: Cannot modify unmodifiable map

Best Practices

Source

Dart UnmodifiableMapBase Documentation

This tutorial covered Dart's UnmodifiableMapBase with practical examples demonstrating immutable map implementations and 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.