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

CLI tool that generates dependencyInjectionFactory() constructors for @dependencyInjectable classes.

singleton_manager_generator #

A CLI tool that generates a dependencyInjectionFactory() constructor for classes annotated with @dependencyInjectable, writing it directly into the annotated class itself.

Features #

  • Scans Dart source files for classes annotated with @dependencyInjectable
  • Reads the class's default (unnamed) constructor and generates a matching factory <ClassName>.dependencyInjectionFactory({String key = 'default', String subkey = 'default'})
  • Each constructor parameter is resolved from the RegistryManagergetInstance<T>(key: key) for non-nullable types, getInstanceNullable<T>(key: key) for nullable ones
  • Parameters tagged @Subkey('...') are resolved with that subkey instead of 'default', disambiguating parameters that share an interface type
  • Parameters tagged @Subkey.inherited() are resolved with the factory's own subkey argument instead of a literal, so a class connected under multiple subkeys can have each variant transitively pull its own matching nested dependencies
  • Preserves the original parameter shape: positional-required, positional-optional ([...]), or named (optionally required)
  • Resolves the type of field-formal parameters (this._field) with no explicit type from the matching field declaration
  • Writes the factory right after the default constructor, directly in the source file — no separate _di.dart file, and both constructors stay grouped before any field/method (satisfying the sort_constructors_first lint)
  • Every generated line carries a trailing // GENERATED CODE - DO NOT MODIFY BY HAND comment — not just a header above the block, each line individually
  • Idempotent: running the generator again fully replaces the previously generated block (factory + trailing members) — whether the constructor's parameters changed shape or the factory had been left in the wrong place, no duplication
  • Automatically inserts the singleton_manager import if it's missing
  • Minimal overhead — uses Dart's analyzer package for lightweight AST parsing

Installation #

Add the packages to your pubspec.yaml:

dependencies:
  singleton_manager: ^2.2.0

dev_dependencies:
  singleton_manager_generator: ^2.2.0

Global Installation #

To install singleton_manager_generator globally and use it across multiple projects:

dart pub global activate singleton_manager_generator

Then use it directly from anywhere:

singleton_manager_generator --input lib

To update the global installation:

dart pub global activate singleton_manager_generator

To deactivate the global installation:

dart pub global deactivate singleton_manager_generator

Usage #

1. Annotate your class #

import 'package:singleton_manager/singleton_manager.dart';

abstract class IPluto {}
abstract class IPaperino {}
abstract class ITopolino {}

@dependencyInjectable
class Pippo {
  IPluto _pluto;
  IPaperino? _paperino;
  ITopolino? _topolino;

  Pippo(
    this._pluto, {
    IPaperino? paperino,
    ITopolino? topolino,
  }) {
    _paperino = paperino;
    _topolino = topolino;
  }
}

2. Run the generator #

dart run singleton_manager_generator --input lib

Or use melos if set up in your workspace:

melos run generate

3. The generated factory is written directly into your class #

@dependencyInjectable
class Pippo {
  IPluto _pluto;
  IPaperino? _paperino;
  ITopolino? _topolino;

  Pippo(
    this._pluto, {
    IPaperino? paperino,
    ITopolino? topolino,
  }) {
    _paperino = paperino;
    _topolino = topolino;
  }

  factory Pippo.dependencyInjectionFactory({String key = 'default', String subkey = 'default'}) { // GENERATED CODE - DO NOT MODIFY BY HAND
    final pluto = RegistryManager.instance.getInstance<IPluto>(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
    final paperino = RegistryManager.instance.getInstanceNullable<IPaperino>(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
    final topolino = RegistryManager.instance.getInstanceNullable<ITopolino>(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND

    return Pippo( // GENERATED CODE - DO NOT MODIFY BY HAND
      pluto, // GENERATED CODE - DO NOT MODIFY BY HAND
      paperino: paperino, // GENERATED CODE - DO NOT MODIFY BY HAND
      topolino: topolino, // GENERATED CODE - DO NOT MODIFY BY HAND
    ); // GENERATED CODE - DO NOT MODIFY BY HAND
  } // GENERATED CODE - DO NOT MODIFY BY HAND
}

The factory is always inserted (or, on a re-run, fully replaced) right after the default constructor — before any field or method declared after it, which get shifted down right after the factory, unchanged. Every non-blank generated line carries the trailing // GENERATED CODE - DO NOT MODIFY BY HAND marker shown above.

Use it once the dependencies are connected:

final registry = RegistryManager.instance;

registry.connectInstance<IPluto, Pluto>(Pluto.dependencyInjectionFactory);
registry.connectInstance<IPaperino, Paperino>(Paperino.dependencyInjectionFactory);
registry.connectInstance<ITopolino, Topolino>(Topolino.dependencyInjectionFactory);

final pippo = Pippo.dependencyInjectionFactory();

How parameters map to the factory #

Constructor parameter shape Generated factory behavior
Non-nullable type (IPluto _pluto) RegistryManager.instance.getInstance<IPluto>(key: key)
Nullable type (IPaperino? paperino) RegistryManager.instance.getInstanceNullable<IPaperino>(key: key)
Tagged @Subkey('...') Adds subkey: '...' to the getInstance/getInstanceNullable call
Tagged @Subkey.inherited() Adds subkey: subkey — forwards the factory's own subkey argument instead of a literal
Positional-required Passed positionally
Positional-optional ([...]) Passed positionally
Named ({...}), optionally required Passed by name: name: localVar
Field-formal param with no explicit type (this._field) Type looked up from the matching field declaration

{String key = 'default', String subkey = 'default'} is always added to the generated factory's own signature, and key is threaded through to every getInstance/getInstanceNullable call inside it — so MyClass.dependencyInjectionFactory(key: 'prod') resolves the whole dependency subtree under 'prod'. subkey is only read inside the body for parameters tagged @Subkey.inherited(); a class connected multiple times under different subkeys (via a hand-written connectInstance call per subkey — the generator can't infer this) can pass that same subkey into its own factory call, so @Subkey.inherited() parameters transitively follow it. See the "Propagating a subkey through nested dependencies" section of the singleton_manager README for the full pattern.

CLI Options #

--input, -i             Input directory containing source Dart files (default: lib)
--output, -o            Output directory to write the rewritten files to (default: same as input — in-place rewrite)
--registry-output, -r   Write a .dart file at this path that registers every discovered singleton into RegistryManager (skipped if omitted)
--verbose, -v           Enable verbose logging
--help, -h              Show help message

By default (no --output, or --output equal to --input), the generator rewrites the actual source files in place.

Generating a registry file #

Pass --registry-output to also generate a .dart file that wires every discovered @dependencyInjectable class into RegistryManager, so you don't have to hand-write the connectInstance calls:

dart run singleton_manager_generator --input lib --registry-output lib/singleton_registry.g.dart

<ProjectName> (PascalCase) is auto-detected from the nearest pubspec.yaml's name: field, walking up from --input — e.g. my_app becomes MyApp. For the Pippo/IPluto/IPaperino/ITopolino example above, in a package named my_app, this produces:

// GENERATED CODE - DO NOT MODIFY BY HAND

import 'package:singleton_manager/singleton_manager.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND

import 'paperino.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'pippo.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'pluto.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND
import 'topolino.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND

/// ... doc comment ...
mixin MainInjectionMyAppMixin { // GENERATED CODE - DO NOT MODIFY BY HAND
  /// Called by [registerAllSingletonsMyApp] right before it connects anything.
  /// Override to customize. // GENERATED CODE - DO NOT MODIFY BY HAND
  void beforeRegisterAllSingletonsMyApp({String key = 'default'}) {} // GENERATED CODE - DO NOT MODIFY BY HAND

  /// Connects every discovered singleton under [key]. // GENERATED CODE - DO NOT MODIFY BY HAND
  void registerAllSingletonsMyApp({String key = 'default'}) { // GENERATED CODE - DO NOT MODIFY BY HAND
    beforeRegisterAllSingletonsMyApp(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
    RegistryManager.instance // GENERATED CODE - DO NOT MODIFY BY HAND
      ..connectInstance<IPluto, Pluto>(() => Pluto.dependencyInjectionFactory(key: key), key: key) // GENERATED CODE - DO NOT MODIFY BY HAND
      ..connectInstance<IPaperino, Paperino>(() => Paperino.dependencyInjectionFactory(key: key), key: key) // GENERATED CODE - DO NOT MODIFY BY HAND
      ..connectInstance<ITopolino, Topolino>(() => Topolino.dependencyInjectionFactory(key: key), key: key) // GENERATED CODE - DO NOT MODIFY BY HAND
      ..connectInstance<Pippo, Pippo>(() => Pippo.dependencyInjectionFactory(key: key), key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
    afterRegisterAllSingletonsMyApp(key: key); // GENERATED CODE - DO NOT MODIFY BY HAND
  } // GENERATED CODE - DO NOT MODIFY BY HAND

  /// Called by [registerAllSingletonsMyApp] right after it finishes connecting
  /// everything. Override to customize. // GENERATED CODE - DO NOT MODIFY BY HAND
  void afterRegisterAllSingletonsMyApp({String key = 'default'}) {} // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND

class MainInjectionMyApp with MainInjectionMyAppMixin { // GENERATED CODE - DO NOT MODIFY BY HAND
  const MainInjectionMyApp(); // GENERATED CODE - DO NOT MODIFY BY HAND
} // GENERATED CODE - DO NOT MODIFY BY HAND

(imports are sorted alphabetically and deduped; when no classes are discovered, registerAllSingletons<ProjectName>'s body is just a // No @dependencyInjectable classes were discovered. comment — no unused variable is emitted.)

Call const MainInjectionMyApp().registerAllSingletonsMyApp() once at startup instead of connecting each type by hand. Every method — before-hook, register, after-hook — is a regular, overridable instance method: subclass MainInjectionMyApp (or mix MainInjectionMyAppMixin into your own class) to hook into the registration lifecycle, e.g. to hand-wire a dependency the generator can't discover on its own (a plain class that isn't @dependencyInjectable):

class AppMainInjection extends MainInjectionMyApp {
  @override
  void beforeRegisterAllSingletonsMyApp({String key = 'default'}) {
    RegistryManager.instance.connectInstance<IExternalConfig, EnvConfig>(
      () => EnvConfig.fromEnvironment(),
      key: key,
    );
  }
}

void main() {
  AppMainInjection().registerAllSingletonsMyApp();
  // ...
}

Each call is independent per key — calling registerAllSingletonsMyApp(key: 'a') and registerAllSingletonsMyApp(key: 'b') sets up two separate singleton graphs side by side, without one overwriting the other.

The type each class is registered under is the first interface in its implements clause; a class implementing nothing is registered under its own type. A class annotated with @DependencyInjectable(subkey: '...') instead of the plain @dependencyInjectable shorthand is connected under that subkey — use this when two @dependencyInjectable classes implement the same interface and both need to be auto-wired without one overwriting the other. The generated connectInstance call also forwards that subkey into the class's own factory call (ClassName.dependencyInjectionFactory(key: key, subkey: '...')), so any of its own @Subkey.inherited() parameters see it too. Import paths are computed relative to the registry file's own location, so it can be placed anywhere.

Requirements #

  • Dart SDK >= 3.11.0
  • package:singleton_manager ^2.2.0 (for @dependencyInjectable, @Subkey, and RegistryManager)
  • package:analyzer for AST parsing
  • package:args for CLI argument handling
  • package:path for path utilities

How it works #

  1. Parsing: Scans Dart files using the analyzer package to find classes annotated with @dependencyInjectable and their default constructor's parameters
  2. Generation: Builds a factory <ClassName>.dependencyInjectionFactory() body that resolves each parameter from RegistryManager.instance and forwards it to the constructor
  3. Rewriting: Inserts (or fully replaces, on a re-run) the factory right after the default constructor, and ensures the singleton_manager import is present
  4. Output: Writes the rewritten file back to disk — in place by default, or under --output if given

Example Project #

See example/my_service.dart and the test/ directory for further examples.

0
likes
0
points
573
downloads

Publisher

unverified uploader

Weekly Downloads

CLI tool that generates dependencyInjectionFactory() constructors for @dependencyInjectable classes.

Repository (GitHub)
View/report issues

Topics

#singleton #dependency-injection #code-generation #annotations

License

unknown (license)

Dependencies

analyzer, args, path

More

Packages that depend on singleton_manager_generator