scoped_deps 0.2.1 copy "scoped_deps: ^0.2.1" to clipboard
scoped_deps: ^0.2.1 copied to clipboard

A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.

Scoped Deps #

A simple dependency injection library built on Zones.

read resolves bindings from an internal registration table that tracks each [Zone] established by this library, so it is not fooled by [Zone]'s inherited [] behavior (which can copy a parent's [ScopedRef] into a middle zone and mask a child's override). For subprocess or other IO that may resume outside that zone, combine with [Zone.bindUnaryCallback] (or related APIs) when registering native callbacks.

Quick Start #

import 'package:scoped_deps/scoped_deps.dart';

final value = create(() => 42);

void main() {
  runScoped(scopeA, values: {value});
}

void scopeA() {
  print(read(value)); // 42
  runScoped(scopeB, values: {value.overrideWith(() => 0)});
}

void scopeB() {
  print(read(value)); // 0
}

Calling read(valueProvider) at every call site works, but it spreads ScopedRefs through code that shouldn't need to know they exist. Pair each ScopedRef with a top-level getter instead, and let the rest of the app read it like an ordinary variable:

final valueProvider = create(() => 42);
int get value => read(valueProvider);
void main() {
  runScoped(() {
    print(value); // 42
  }, values: {valueProvider});
}

Anything that reads value — no matter how deeply nested — resolves against whichever scope is active when it runs. Callers never import ScopedRef or call read directly, which also makes the pattern straightforward to test (see Testing below).

API #

  • create(() => value) — declare a ScopedRef<T>.
  • read(ref, {orElse}) — resolve ref in the current scope; throws a StateError (or calls orElse) if no scope provides it.
  • isRegistered(ref) — check whether ref is bound in the current scope, without the StateError/orElse path.
  • runScoped(body, {values}) — run body in a scope that provides values.
  • runMergedScoped(body, {override, includeIfAbsent}) — run body in a scope forked from the current one. override replaces the current binding for those refs; includeIfAbsent adds a binding only for refs the current scope chain doesn't already define.
  • runMergedScopedFuture(body, {override, includeIfAbsent, zoneSpecification}) — like runMergedScoped, but requires an explicitly async body and accepts an optional zoneSpecification (e.g. to intercept print). override / includeIfAbsent survive await in runMergedScoped too — reach for this variant when you want the stricter async signature or need zoneSpecification, not because bindings would otherwise be lost.
  • runScopedGuarded(body, {onError, values}) — like runScoped, but uncaught errors are routed to onError instead of thrown.
  • ref.overrideWith(() => other) — a ref with the same identity as ref but a different value factory, used to substitute a value for a nested scope (or in a test).

Testing #

Bindings live on the Zone, so they don't exist until something establishes a scope — code under test has to run inside a runScoped / runMergedScoped call that provides whatever it reads. A setUp can't do this on its own: it runs to completion before the test body starts, so any scope it forks is already gone by the time your test executes. Wrap the test body itself:

final valueProvider = create(() => 42);
int get value => read(valueProvider);

int doubleValue() => value * 2;

test('doubleValue uses the overridden value', () {
  runScoped(() {
    expect(doubleValue(), equals(20));
  }, values: {valueProvider.overrideWith(() => 10)});
});

The override applies no matter how deep the call chain is — doubleValue() never has to know it's under test. The same works for async code under test; the override stays bound across every await:

Future<int> asyncDoubleValue() async {
  await Future<void>.delayed(Duration.zero);
  return value * 2;
}

test('asyncDoubleValue uses the overridden value', () async {
  await runScoped(() async {
    expect(await asyncDoubleValue(), equals(20));
  }, values: {valueProvider.overrideWith(() => 10)});
});

Reach for runMergedScopedFuture instead when a test also needs a zoneSpecification — for example, to capture print output for the duration of the test rather than letting it hit the console.

8
likes
160
points
128
downloads

Documentation

API reference

Publisher

verified publishermrgnhnt.com

Weekly Downloads

A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.

Repository (GitHub)
View/report issues

Topics

#dependency-injection #architecture #dart

License

MIT (license)

Dependencies

meta

More

Packages that depend on scoped_deps