singleton_manager 2.2.2
singleton_manager: ^2.2.2 copied to clipboard
High-performance singleton manager for Dart with DI annotations and lazy loading.
singleton_manager #
A minimal, zero-dependency dependency-injection registry for Dart:
RegistryManager, the IRegistry interface it implements, and the
@dependencyInjectable annotation used by
singleton_manager_generator to write
factory constructors for you.
Features #
- Type-safe: generic
getInstance<T>()/connectInstance<Interface, Impl>() - Zero dependencies: no external package dependencies at runtime
- Lazy: a connected factory runs once, the first time its type is resolved; the result is then cached
- Per-key registration: register multiple instances of the same type under different string keys (default key:
'default') - Per-subkey disambiguation: register two instances of the same type under the same key — e.g. two implementations of one interface — via an independent
subkey(default:'default') - Subkey propagation:
@Subkey.inherited()lets a class connected under multiple subkeys transitively resolve its own nested dependencies under that same subkey, instead of a fixed one - Code generation ready: annotate a class with
@dependencyInjectable, let the generator write itsdependencyInjectionFactory() - Pure Dart: no platform-specific code, works on VM, Web, and Flutter
Installation #
Add to your pubspec.yaml:
dependencies:
singleton_manager: ^2.2.1
Quick Start #
import 'package:singleton_manager/singleton_manager.dart';
abstract class ILogger {
void log(String message);
}
class ConsoleLogger implements ILogger {
@override
void log(String message) => print(message);
}
void main() {
final registry = RegistryManager.instance;
// Connect an interface to a factory — invoked lazily, once, on first access.
registry.connectInstance<ILogger, ConsoleLogger>(() => ConsoleLogger());
// Resolve it (same cached instance every time after the first call).
final logger = registry.getInstance<ILogger>();
logger.log('Hello from ConsoleLogger!');
}
The IRegistry API #
abstract class IRegistry {
/// Returns the instance connected to [InterfaceType] under [key]/[subkey],
/// creating it via its connected factory on first access. Throws
/// `RegistryNotFoundError` if nothing is connected.
InterfaceType getInstance<InterfaceType>({
String key = 'default',
String subkey = 'default',
});
/// Same as [getInstance] but returns `null` instead of throwing.
InterfaceType? getInstanceNullable<InterfaceType>({
String key = 'default',
String subkey = 'default',
});
/// Connects [InterfaceType] to a concrete [InstanceType] via [factory].
/// The factory runs once, the first time [getInstance] (or
/// [getInstanceNullable]) is called for this (type, key, subkey) triple;
/// the result is cached and reused after that.
void connectInstance<InterfaceType, InstanceType extends InterfaceType>(
InstanceType Function() factory, {
String key = 'default',
String subkey = 'default',
});
/// Directly sets [instance] as the value connected to [InterfaceType]
/// under [key]/[subkey], overwriting any existing instance or connected
/// factory's cached result for that same (type, key, subkey).
void setInstance<InterfaceType>(
InterfaceType instance, {
String key = 'default',
String subkey = 'default',
});
}
RegistryManager is a singleton implementation of IRegistry, accessed via
RegistryManager.instance. Every instance/factory is stored under the
compound key (Type, key, subkey) — key and subkey are independent axes,
each defaulting to 'default'.
Setting an instance directly #
Use setInstance when you already have an instance in hand — e.g. wiring a
mock in a test, or a value produced elsewhere — instead of going through
connectInstance's lazy factory:
final registry = RegistryManager.instance;
registry.setInstance<ILogger>(FakeLogger());
final logger = registry.getInstance<ILogger>(); // returns the FakeLogger
It overwrites whatever was previously connected/set for that
(Type, key, subkey) triple — including a connected factory's already-cached
result.
Per-key registration #
Use key to keep more than one instance of the same type — e.g.
environment-specific connections:
final registry = RegistryManager.instance;
registry.connectInstance<IDbConnection, PostgresConnection>(
() => PostgresConnection('postgres://prod'),
key: 'prod',
);
registry.connectInstance<IDbConnection, PostgresConnection>(
() => PostgresConnection('postgres://dev'),
key: 'dev',
);
final prod = registry.getInstance<IDbConnection>(key: 'prod');
final dev = registry.getInstance<IDbConnection>(key: 'dev');
Per-subkey disambiguation #
Use subkey when you need two (or more) instances of the same interface,
under the same key — e.g. a constructor with two parameters that share a
type:
abstract class IStunHandler {
String resolve();
}
final registry = RegistryManager.instance;
registry.connectInstance<IStunHandler, Ipv4StunHandler>(
Ipv4StunHandler.new,
subkey: 'ipv4',
);
registry.connectInstance<IStunHandler, Ipv6StunHandler>(
Ipv6StunHandler.new,
subkey: 'ipv6',
);
final ipv4Handler = registry.getInstance<IStunHandler>(subkey: 'ipv4');
final ipv6Handler = registry.getInstance<IStunHandler>(subkey: 'ipv6');
See example/3_subkey_dependency_injection.dart
for the full pattern, including the @Subkey('...') annotation that lets
singleton_manager_generator wire this up automatically.
Error handling #
| Situation | Result |
|---|---|
getInstance<T>() and nothing was connected/set for (T, key, subkey) |
throws RegistryNotFoundError |
getInstanceNullable<T>() and nothing was connected/set for (T, key, subkey) |
returns null |
Code generation with @dependencyInjectable #
Annotate a class and run singleton_manager_generator to have its
dependencyInjectionFactory({String key = 'default', String subkey = 'default'}) written for you, instead of hand-writing the constructor call
and its RegistryManager lookups:
@dependencyInjectable
class OrderService implements IOrderService {
OrderService(this._repository, {IClock? clock});
final IOrderRepository _repository;
}
Tag a constructor parameter with @Subkey('...') when it shares an interface
type with another parameter, so each is resolved under its own subkey instead
of colliding on 'default':
@dependencyInjectable
class DualStunHandler {
DualStunHandler({
@Subkey('ipv4') IStunHandler? ipv4Handler,
@Subkey('ipv6') IStunHandler? ipv6Handler,
});
}
Tag the class itself with @DependencyInjectable(subkey: '...') instead of
the plain @dependencyInjectable shorthand when the generated
--registry-output registry must connect that class under a specific
subkey — typically because two @dependencyInjectable classes implement the
same interface and both need to be auto-wired:
@DependencyInjectable(subkey: 'eu')
class EuEndpoint implements IRegionEndpoint {}
@DependencyInjectable(subkey: 'us')
class UsEndpoint implements IRegionEndpoint {}
Propagating a subkey through nested dependencies #
A literal @Subkey('...') only disambiguates the immediate parameter — it
says nothing about that parameter's own dependencies further down the graph.
Use @Subkey.inherited() instead of a literal value when a class must be
connected multiple times (once per subkey) and each variant should
transitively resolve its own nested dependencies under that same subkey,
rather than always the same one:
abstract class IPart {}
@DependencyInjectable(subkey: 'ipv4')
class Ipv4Part implements IPart {}
@DependencyInjectable(subkey: 'ipv6')
class Ipv6Part implements IPart {}
@dependencyInjectable
class Single {
Single(@Subkey.inherited() IPart part);
}
Single itself carries no fixed @DependencyInjectable(subkey: ...) — it's
one generic class meant to serve every variant. Connect it once per subkey by
hand (e.g. in an overridden beforeRegisterAllSingletons<ProjectName>),
passing that same subkey into its own factory call:
RegistryManager.instance
..connectInstance<ISingle, Single>(
() => Single.dependencyInjectionFactory(key: key, subkey: 'ipv4'),
key: key,
subkey: 'ipv4',
)
..connectInstance<ISingle, Single>(
() => Single.dependencyInjectionFactory(key: key, subkey: 'ipv6'),
key: key,
subkey: 'ipv6',
);
Each connected variant then resolves IPart via
getInstance<IPart>(key: key, subkey: subkey) — the 'ipv4'-registered
Single pulls Ipv4Part, the 'ipv6'-registered one pulls Ipv6Part —
without Single needing two hand-duplicated subclasses.
The propagation composes across more than one level, and doesn't require
manual wiring at every level — only where a class has no fixed subkey of
its own. Extend IPart with one more nested dependency:
abstract class IUnit {}
@DependencyInjectable(subkey: 'ipv4')
class Ipv4Unit implements IUnit {}
@DependencyInjectable(subkey: 'ipv6')
class Ipv6Unit implements IUnit {}
@DependencyInjectable(subkey: 'ipv4')
class Ipv4Part implements IPart {
Ipv4Part(@Subkey.inherited() IUnit unit);
}
@DependencyInjectable(subkey: 'ipv6')
class Ipv6Part implements IPart {
Ipv6Part(@Subkey.inherited() IUnit unit);
}
Unlike Single, Ipv4Part/Ipv6Part each carry their own
@DependencyInjectable(subkey: ...), so --registry-output connects both
automatically — no manual connectInstance call needed for this level. It
already forwards each class's own subkey into its generated factory call
(Ipv4Part.dependencyInjectionFactory(key: key, subkey: 'ipv4')), so their
@Subkey.inherited() parameter picks up Ipv4Unit/Ipv6Unit for free. Only
Single needs hand-wiring, because it's the one class in the chain with no
fixed subkey of its own — everything below it follows automatically.
See singleton_manager_generator
for how the generator maps constructor parameters to getInstance/
getInstanceNullable calls, and for the --registry-output flag that also
generates a MainInjection<ProjectName> class (with overridable
beforeRegisterAllSingletons<ProjectName> / registerAllSingletons<ProjectName> /
afterRegisterAllSingletons<ProjectName> methods) wiring everything up.
Examples #
See the examples directory for complete, runnable examples:
- 1_basic_registry.dart:
RegistryManager,connectInstance,getInstance,getInstanceNullable - 2_dependency_injection_factory.dart:
@dependencyInjectableand the generateddependencyInjectionFactory() - 3_subkey_dependency_injection.dart:
@Subkey('...')disambiguating two constructor parameters that share an interface
cd packages/singleton_manager
dart run example/1_basic_registry.dart
dart run example/2_dependency_injection_factory.dart
dart run example/3_subkey_dependency_injection.dart
Documentation #
For more information, see the main project documentation.
Contributing #
See CONTRIBUTING.md for contribution guidelines.
License #
MIT License - see LICENSE file for details.