poly_registry
A type-safe polymorphic registry for Dart. Look up objects by key or Dart type and convert polymorphic maps using registered converters — no code generation required.
Features
- 🔑 Look up entries by key or by Dart type
- 🔄 Built-in polymorphic Map conversion via
MapConverterRegistry - 🛡️ Configurable duplicate key handling — keep first, keep last, or throw
- 🧩 Extensible — build your own registry on top of
PolyRegistryorConverterRegistry
Motivation
I've been writing Flutter apps for a while, and the same pattern kept
showing up across every project: a Map<String, Function> buried somewhere
in the codebase, doing the job of a registry.
It usually starts small — a map of JSON deserializers, or a map of route handlers. You add a key, wire up a function, move on. But as the project grows, that map grows with it. Then you need a second one for a different purpose. Then a third. Before long you have several of these scattered across the codebase, each with slightly different error handling, each reinventing the same lookup logic, and none of them type-safe enough to catch mistakes at compile time.
I kept extracting the same abstraction by hand in project after project
until it became obvious it should just be a package. poly_registry is
that abstraction — a typed, reusable registry that handles lookup, duplicate
key strategies, and polymorphic conversion in one place, so I never have to
write another raw Map<String, Function> again.
Installation
Add poly_registry to your pubspec.yaml:
dependencies:
poly_registry: ^1.0.0
Then run:
dart pub get
Quick start
The most common use case is polymorphic Map deserialization — converting a Map object to one of several concrete types based on a discriminator field:
// 1. Define your models
abstract class JobPayload {
Map<String, dynamic> toMap();
}
const typeKey = 'type';
class VideoPayload extends JobPayload {
VideoPayload({required this.filePath});
factory VideoPayload.fromMap(Map<dynamic, dynamic> from) => VideoPayload(filePath: from['filePath'] as String);
final String filePath;
static const type = 'video';
@override
Map<String, dynamic> toMap() => {typeKey: type, 'filePath': filePath};
}
class DataSyncPayload extends JobPayload {
DataSyncPayload({required this.endpoint});
factory DataSyncPayload.fromMap(Map<dynamic, dynamic> from) => DataSyncPayload(endpoint: from['endpoint'] as String);
final String endpoint;
static const type = 'dataSync';
@override
Map<String, dynamic> toMap() => {typeKey: type, 'endpoint': endpoint};
}
// 2. Create the registry
final registry = MapConverterRegistry<JobPayload>.prepare(
typeKey: typeKey,
entries: [
InlineMapConverter(VideoPayload.type, VideoPayload.fromMap),
InlineMapConverter(DataSyncPayload.type, DataSyncPayload.fromMap),
],
);
// 3. Use it.
final videoPayload = registry.convert({
typeKey: 'video',
'filePath': '/tmp/video.mp4',
});
print(videoPayload); // Instance of 'VideoPayload'
final dataSyncPayload = registry.convert({
typeKey: 'dataSync',
'endpoint': '/sync',
});
print(dataSyncPayload); // Instance of 'DataSyncPayload'
final List<JobPayload> payloadList = [
VideoPayload(filePath: '/tmp/video.mp4'),
DataSyncPayload(endpoint: '/sync'),
];
final List<Map<String, dynamic>> mapList = payloadList.map((e) => e.toMap()).toList();
final restoredList = registry.convertAll(mapList).toList();
print(restoredList.length); // 2
Usage
Basic registry
Use PolyRegistry to store and retrieve any objects that have a unique key:
class RouteHandler {
const RouteHandler(this.handle);
final void Function() handle;
}
final registry = PolyRegistry<String, RouteHandler>(
entries: {
'home': RouteHandler(() => print('Home')),
'profile': RouteHandler(() => print('Profile')),
},
);
registry.getByKey('home').handle(); // Home
print(registry.contains('profile')); // true
Batch conversion
final items = [
{'type': 'video', 'filePath': '/tmp/a.mp4'},
{'type': 'dataSync', 'endpoint': 'https://api.example.com'},
{'type': 'unknown'}, // no converter registered — will be skipped
];
final registry = MapConverterRegistry<JobPayload>.prepare(
typeKey: 'type',
entries: [
InlineMapConverter(VideoPayload.type, VideoPayload.fromMap),
InlineMapConverter(DataSyncPayload.type, DataSyncPayload.fromMap),
],
);
// Skip failures and handle errors
final payloads = registry.convertAll(items, (item, error, stack) {
print('Failed: $item — $error');
});
// Or throw on first failure
final payloads = registry.convertAllOrThrow(items);
Duplicate key strategies
MapConverterRegistry<JobPayload>.prepare(
typeKey: 'type',
entries: [
InlineMapConverter(VideoPayload.type, VideoPayload.fromMap),
InlineMapConverter(VideoPayload.type, VideoPayload.fromMap),
],
duplicateStrategy: DuplicateKeyStrategy.keepFirst, // default
// duplicateStrategy: DuplicateKeyStrategy.keepLast,
// duplicateStrategy: DuplicateKeyStrategy.throwError,
onDuplicate: (entry) => print('Duplicate key: ${entry.registryKey}'),
);
Runtime registration
registry.register('settings', RouteHandler(() => print('Settings')));
registry.unregister('settings');
Custom registry for any format
Extend ConverterRegistry to support any source format beyond Map:
abstract class ProtobufConverter<TO> extends PolyConverter<Uint8List, TO> {}
class ProtobufRegistry<TO> extends ConverterRegistry<int, Uint8List, TO, ProtobufConverter<TO>> {
ProtobufRegistry({required super.entries});
@override
int extractRegistryKey(Uint8List from) => from[0]; // message type from first byte
}
License
MIT