Cloner

Deep cloning utilities for Dart collections and custom types.

Latest Release pipeline status Report Bug

BSD 3-Clause License Dart 3.9.2

Features ✨

  • Element/value-wise deep cloning for List, Set, and Map
  • ICloneable interface for custom types with deep-clone support
  • Collection extensions providing clone() and cloneDynamic() methods
  • MapClone wrapper for typed nested map cloning
  • SetClone wrapper for typed nested set cloning
  • Optional circular reference detection (opt-in)
  • Tries to preserve concrete collection types (LinkedHashMap, HashMap, etc.)
  • Custom Cloner implementations using ICloning
  • Pluggable cloner architecture: swap or customize the global cloner

Usage

Basic Import

import 'package:cloner/cloner.dart';

Or import only what you need:

import 'package:cloner/core.dart';       // Core cloning API
import 'package:cloner/base.dart';       // for custom cloning strategies
import 'package:cloner/extensions.dart'; // Collection extensions + collection wrappers

Implementing ICloneable

Define custom types that support deep cloning:

class Address implements ICloneable<Address> {
  String street;
  int number;

  Address(this.street, this.number);

  @override
  Address clone([ICloning? cloner]) => Address(street, number);
}

class Person implements ICloneable<Person> {
  String name;
  Address address;

  Person(this.name, this.address);

  @override
  Person clone([ICloning? cloner]) => Person(name, address.clone(cloner));
}

Cloning

The Cloner facade provides a pluggable, global cloner reference and supports custom options.

Basic deep cloning

final list = [1, 2, 3];
final cloned = Cloner.instance().cloneList(list);
cloned[0] = 99;
print(list);   // [1, 2, 3]
print(cloned); // [99, 2, 3]

Swapping the global cloner

You can replace the global cloner for all subsequent operations:

Cloner.builder = MyCustomClonerBuilder();
final MyCustomCloner myCloner = Cloner.instance();

Collection Extensions

Use clone() for typed cloning or cloneDynamic() for untyped cloning. Both respect the current Cloner.builder and accept an optional cloner parameter (ICloning). For nested typed structures, use MapClone and SetClone to preserve concrete collection types during typed cloning.

// List
final numbers = [1, 2, 3];
final clonedNumbers = numbers.clone(); // List<int>

// Set
final tags = {'a', 'b', 'c'};
final clonedTags = tags.clone(); // Set<String>

// Map
final scores = {'alice': 100, 'bob': 95};
final clonedScores = scores.clone(); // Map<String, int>

// Dynamic clone for heterogeneous collections
final mixed = [1, 'two', {'key': 'value'}];
final clonedMixed = mixed.cloneDynamic(); // List<dynamic>

final cloneTyped = numbers.clone();

Nested Typed Collections

For deep cloning of nested, typed collections while preserving their concrete types, use MapClone for maps and SetClone for sets. These wrappers ensure that both the type information and the runtime collection implementation are retained during cloning.

Example: Deep Cloning Nested Typed Maps

final config = MapClone<String, MapClone<String, int>>.ofMap({
  'limits': MapClone.ofMap({'maxRetries': 3, 'timeout': 30}),
});

final cloned = config.clone();
cloned['limits']!['maxRetries'] = 10;

print(config['limits']!['maxRetries']); // 3 (unchanged)
print(cloned['limits']!['maxRetries']); // 10

MapClone also provides copy() for shallow copies:

final original = MapClone<String, List<int>>();
original['data'] = [1, 2, 3];

final shallow = original.copy();  // Lists are shared
final deep = original.clone();    // Lists are cloned

Worth Noting 🔍

Typed vs Dynamic Cloning

  • Typed cloning (clone()) preserves generic types but throws UnsupportedTypedCloneException if a nested plain Map is encountered. Wrap nested maps with MapClone for typed cloning.

  • Dynamic cloning (cloneDynamic()) returns dynamic element/value types and handles any nested structure including plain maps.

Concrete Type Preservation

When cloning collections, BaseCloner preserves the concrete type of built-in Dart collections. For example, if you clone a LinkedHashMap, HashSet, or an UnmodifiableListView, the cloned result will be of the same type, not just a generic Map, Set, or List. This ensures that collection-specific behaviors and ordering are retained in the clone. For nested typed sets, use SetClone to preserve the runtime element type alongside the set implementation during typed cloning.

Important

Custom collections and SplayTreeMap, SplayTreeSet, or any are not automatically supported for type-preserving deep cloning. To ensure correct cloning of these types, implement the ICloneable interface for your collection, or accept that they will be cloned as a default Map, Set, or List (losing their specific behaviors).

Refer to public API docs.

Problem-Solution 🤔 (from the maintainers)

static Cloner does not work with isolates (multithreading)

Use cloner implementations directly (imported from package:cloner/base.dart):

BaseCloner cloner = (BaseClonerBuilder()..doTypedClone = true).build();
cloner.cloneMap(map);
cloner.cloneValue(value);
cloner.cloneList(list);

When it comes to collection extensions, if they are going to be used in a separate isolate, don't forget to swap Cloner.builder (Dart isolates have separate memory, so there is no retention of static global state).

Note

When working with isolates, it is possible to serialize cloner builder configuration in the main isolate, send it through a SendPort, and then deserialize it in the spawned isolate. This ensures both isolates use the same cloner configuration without sharing static state. Example:

// In main isolate
final builder = CountedClonerBuilder()..doTypedClone = true;
final String serialized = builder.serialize();
sendPort.send(serialized);

// In spawned isolate
final received = await receivePort.first as String;
final cloner = BaseClonerBuilder().deserialize(received);

Low performance on collection cloning

Follow these practices to squeeze all the juice out of Cloner:

Loops

Each call to instance() instantiates a new object using the builder, so the fewer times it is called, the better – avoid calling it inside loops (this applies to collection extensions as well).

Right tool for the task

  • There is no need to complicate things. No possibility of circular references or enormous collection sizes means no need of counting overhead, just use BaseCloner
  • Prefer typed over dynamic cloning (except for Map: it is actually beneficial to dynamically clone maps). Simple lists and sets are cloned faster using typed modes.

Important

Be aware of how certain cloning methods handle concrete type preservation – see Concrete Type Preservation for more info.

Benchmarks 📊

Methodology

  1. Warm-up: A single clone operation is performed to prime the execution environment.
  2. Iterative Testing: For a defined number of iterations:
    • Timer starts, the clone function executes, and the timer stops.
    • Cloner is reset by rebuilding it completely from the builder to ensure measurement isolation.
  3. Metrics Calculation: min/max/avg metrics are calculated and converted to milliseconds (ms).

Note

Benchmark Configuration:

  • Iterations per set: 6
  • Collection size: 469,969 elements

General Overview

Averages across all cloner implementations:

List Cloning

Generally stays under 50ms. Typed AOT is the fastest at approximately 33ms, while dynamic AOT is the slowest at 46ms.

Set Cloning

Generally takes roughly triple the time of lists, remaining under 170ms. Dynamic AOT is the fastest at approximately 131ms, while typed JIT is the slowest at 164ms.

Map Cloning

Performance is similar to sets but slightly higher, staying under 190ms. Dynamic AOT is the fastest at approximately 166ms, while typed JIT is the slowest at 181ms.

Global Average benchmark table

  • JIT Compilation:
    • Typed collections: AdaptiveCloner
    • Dynamic collections: Performance varies by collection type; avoid HashedCloner for dynamic sets/maps.
  • AOT Compilation:
    • Typed collections: CountedCloner
    • Dynamic collections: PureCloner or CountedCloner; avoid HashedCloner for dynamic sets/maps.

Takeaways

  • AdaptiveCloner dominates typed JIT
  • Use PureCloner for dynamic maps when circular references are not present and preserving the underlying collection type is not required
  • HashedCloner struggles at dynamic sets/maps
  • CountedCloner performs good in AOT

Fastest

The fastest Cloner implementation benchmark table

Slowest

The slowest Cloner implementation benchmark table

Note

Detailed benchmark results available at doc/benchmarks Also see implementation at benchmark/tools

License

BSD 3-Clause License
This work is licensed under the BSD 3-Clause License.

Libraries

base
Base cloning implementation and interface for custom cloning strategies.
cloner
Utilities for producing deep clones
core
Core cloning API and contracts for the Cloner library.
extensions
Collection cloning extensions for List, Set, and Map.