scoped_deps
A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.
A simple Dart library for managing scoped dependencies built on top of Zones from dart:async.
English project snapshot. Visit GitHub for the latest content.
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.
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
}
read behind a getterCalling 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).
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).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.