davianspace_options 1.0.3 copy "davianspace_options: ^1.0.3" to clipboard
davianspace_options: ^1.0.3 copied to clipboard

Strongly typed configuration binding for Flutter apps, Dart CLI tools, backend services, and web applications inspired by Microsoft.Extensions.Options.

davianspace_options #

pub.dev Dart License: MIT

Enterprise-grade Options pattern for Dart and Flutter — conceptually equivalent to Microsoft.Extensions.Options but expressed idiomatically in Dart, with no reflection, no mirrors, and zero external dependencies.


Features #

Concept Dart equivalent Description
IOptions<T> Options<T> Singleton-cached access
IOptionsSnapshot<T> OptionsSnapshot<T> Scoped / per-scope fresh snapshot
IOptionsMonitor<T> OptionsMonitor<T> Live access + change notifications
IOptionsFactory<T> OptionsFactory<T> Factory pipeline with validation
IOptionsMonitorCache<T> OptionsMonitorCache<T> Per-name instance cache
IConfigureOptions<T> ConfigureOptions<T> Configure registrations
IPostConfigureOptions<T> PostConfigureOptions<T> Post-configure registrations
IValidateOptions<T> ValidateOptions<T> Validation registrations
Named options OptionsBuilder.configureNamed() Per-name configuration branches
IChangeToken ChangeToken Change propagation primitives

Installation #

dependencies:
  davianspace_options: ^1.0.3

Quick start #

1 — Simple singleton options (Options<T>) #

import 'package:davianspace_options/davianspace_options.dart';

class DatabaseOptions {
  String host = 'localhost';
  int    port = 5432;
}

void main() {
  final factory = OptionsFactoryImpl<DatabaseOptions>(
    instanceFactory: DatabaseOptions.new,
    configureOptions: [
      ConfigureNamedOptions(
        name: null, // applies to every name
        configure: (opts) {
          opts.host = 'db.example.com';
          opts.port = 5432;
        },
      ),
    ],
  );

  final options = OptionsManager<DatabaseOptions>(factory: factory);

  // Lazily created once, cached forever.
  final db = options.value;
  print(db.host); // db.example.com

  // Same physical instance on repeated access.
  assert(identical(options.value, db));
}

2 — Named options #

final options = OptionsManager<DatabaseOptions>(factory: factory);

final primary = options.get('primary');
final replica  = options.get('replica');

// Different instances, independently configured.

3 — Validation #

final factory = OptionsFactoryImpl<DatabaseOptions>(
  instanceFactory: DatabaseOptions.new,
  validators: [
    DelegateValidateOptions(
      name: null,
      validate: (name, opts) {
        if (opts.host.isEmpty) {
          return ValidateOptionsResult.fail('$name: host is required.');
        }
        return ValidateOptionsResult.success();
      },
    ),
  ],
);

try {
  factory.create(Options.defaultName); // throws if host is empty
} on OptionsValidationException catch (e) {
  for (final msg in e.failures) print(msg);
}

4 — Fluent builder #

final builder = OptionsBuilder<DatabaseOptions>(factory: DatabaseOptions.new)
  ..configure((opts) => opts.host = 'db.example.com')
  ..postConfigure((opts) => opts.port = opts.port == 0 ? 5432 : opts.port)
  ..validate(
    (name, opts) => opts.host.isNotEmpty
        ? ValidateOptionsResult.success()
        : ValidateOptionsResult.fail('$name: host required'),
  );

final factory = OptionsFactoryImpl<DatabaseOptions>(
  instanceFactory: builder.factory,
  configureOptions:     builder.configureActions,
  postConfigureOptions: builder.postConfigureActions,
  validators:           builder.validators,
);

5 — Live change notifications (OptionsMonitor<T>) #

final notifier = OptionsChangeNotifier();

final monitor = OptionsMonitorImpl<DatabaseOptions>(
  factory:  factory,
  notifier: notifier,
);

final registration = monitor.onChange((opts, name) {
  print('$name changed → host=${opts.host}');
});

// Trigger a reload from your app (file watcher, remote config, etc.):
notifier.notifyChange(Options.defaultName);

// Always dispose when done:
registration.dispose();
monitor.dispose();

6 — Scoped snapshot (OptionsSnapshot<T>) #

Create one OptionsManager per logical scope (request, unit-of-work, test) to get a fresh snapshot that remains stable within that scope:

void handleRequest(OptionsFactory<FeatureFlags> factory) {
  // New manager = new scope; instances are created fresh from the factory.
  final snapshot = OptionsManager<FeatureFlags>(factory: factory);

  final flags  = snapshot.value;         // cached within this scope
  final beta   = snapshot.get('beta');   // independent named snapshot

  // ... use flags ...
}

7 — DI container integration (davianspace_dependencyinjection) #

When used with davianspace_dependencyinjection, the Options Pattern is wired into the container via fluent extension methods that automatically register Options<T>, OptionsSnapshot<T>, and OptionsMonitor<T> at the correct lifetimes.

# pubspec.yaml
dependencies:
  davianspace_options: ^1.0.3
  davianspace_dependencyinjection: ^1.0.3
import 'package:davianspace_options/davianspace_options.dart';
import 'package:davianspace_dependencyinjection/davianspace_dependencyinjection.dart';

class DatabaseOptions {
  String host = 'localhost';
  int    port = 5432;
}

final provider = ServiceCollection()
  ..configure<DatabaseOptions>(
    factory: DatabaseOptions.new,
    configure: (opts) {
      opts.host = 'db.prod.internal';
      opts.port = 5432;
    },
  )
  ..postConfigure<DatabaseOptions>((opts) {
    if (opts.host.isEmpty) throw ArgumentError('host is required');
  })
  .buildServiceProvider();

// Inject by interface — DI handles lifetime automatically.
final opts      = provider.getRequired<Options<DatabaseOptions>>().value;
final snapshot  = provider.getRequired<OptionsSnapshot<DatabaseOptions>>().value;
final monitor   = provider.getRequired<OptionsMonitor<DatabaseOptions>>();

// Live reload — signal the keyed notifier registered for the options type.
final notifier =
    provider.getRequiredKeyed<OptionsChangeNotifier>(DatabaseOptions);
notifier.notifyChange(Options.defaultName);

Lifetimes registered automatically:

Injectable type Lifetime
Options<T> Singleton
OptionsSnapshot<T> Scoped
OptionsMonitor<T> Singleton

Architecture #

┌────────────────────────────────────────────────────────────────┐
│  Application layer                                             │
│  OptionsManager  •  OptionsMonitorImpl  •  OptionsBuilder      │
├────────────────────────────────────────────────────────────────┤
│  Factory pipeline                                              │
│  OptionsFactoryImpl                                            │
│    1. instantiate  →  2. configure  →  3. postConfigure        │
│    4. validate  →  (throws OptionsValidationException)         │
├────────────────────────────────────────────────────────────────┤
│  Cache layer                                                   │
│  OptionsMonitorCacheImpl  (per-name Map, O(1) lookup)          │
├────────────────────────────────────────────────────────────────┤
│  Change tracking                                               │
│  ManualChangeToken  •  CompositeChangeToken  •                 │
│  NeverChangeToken   •  OptionsChangeNotifier                   │
└────────────────────────────────────────────────────────────────┘

Migration notes from Microsoft.Extensions.Options #

.NET API Dart equivalent
services.AddOptions<T>() OptionsBuilder<T>(factory: T.new)
.Configure<T>(action) builder.configure(action)
.Configure<T>(name, action) builder.configureNamed(name, action)
.PostConfigure<T>(action) builder.postConfigure(action)
.ValidateDataAnnotations() builder.validate(closureValidator)
IOptions<T>.Value OptionsManager<T>.value
IOptionsSnapshot<T>.Get(name) OptionsManager<T>.get(name)
IOptionsMonitor<T>.CurrentValue OptionsMonitorImpl<T>.currentValue
IOptionsMonitor<T>.OnChange(cb) monitor.onChange(cb) → disposable
IOptionsMonitorCache<T>.Clear() OptionsMonitorCacheImpl<T>.clear()
IChangeToken ChangeToken
CancellationTokenSource ManualChangeToken

Key differences:

  • No reflection or code generation – register a factory closure instead of relying on Activator.CreateInstance.
  • No DI container coupling – the library is container-agnostic; wire it into GetIt, Riverpod, or any other solution.
  • Disposal is explicit – call monitor.dispose() and registration.dispose() rather than relying on IDisposable from a DI container lifetime scope.
  • Scoped snapshot via new instance – create a new OptionsManager per scope instead of registering with Scoped lifetime.

Performance #

  • O(1) retrieval after first creation (hash-map backed cache).
  • No reflection, no dart:mirrors.
  • Factory closures — zero-cost compared to activator pattern.
  • Listener list is a plain List<Function>; add/remove are O(n) but lists are tiny in practice.

License #

MIT — see LICENSE.

0
likes
160
points
142
downloads

Documentation

API reference

Publisher

verified publisherdavian.space

Weekly Downloads

Strongly typed configuration binding for Flutter apps, Dart CLI tools, backend services, and web applications inspired by Microsoft.Extensions.Options.

Repository (GitHub)
View/report issues
Contributing

Topics

#configuration #options #flutter #architecture #app-settings

License

MIT (license)

More

Packages that depend on davianspace_options