singleton_manager 2.2.2 copy "singleton_manager: ^2.2.2" to clipboard
singleton_manager: ^2.2.2 copied to clipboard

High-performance singleton manager for Dart with DI annotations and lazy loading.

singleton_manager #

Pub Version

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 its dependencyInjectionFactory()
  • 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: @dependencyInjectable and the generated dependencyInjectionFactory()
  • 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.

0
likes
160
points
570
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

High-performance singleton manager for Dart with DI annotations and lazy loading.

Repository (GitHub)
View/report issues
Contributing

Topics

#singleton #pattern #dependency-injection #service-locator #resource-management

License

MIT (license)

Dependencies

index_generator

More

Packages that depend on singleton_manager