singleton_manager 2.1.1
singleton_manager: ^2.1.1 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') - 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.1.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',
});
}
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'.
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'}) 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 {}
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.