scoped_deps 0.2.1
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
}
Recommended: hide read behind a getter #
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 aScopedRef<T>.read(ref, {orElse})— resolverefin the current scope; throws aStateError(or callsorElse) if no scope provides it.isRegistered(ref)— check whetherrefis bound in the current scope, without theStateError/orElsepath.runScoped(body, {values})— runbodyin a scope that providesvalues.runMergedScoped(body, {override, includeIfAbsent})— runbodyin a scope forked from the current one.overridereplaces the current binding for those refs;includeIfAbsentadds a binding only for refs the current scope chain doesn't already define.runMergedScopedFuture(body, {override, includeIfAbsent, zoneSpecification})— likerunMergedScoped, but requires an explicitly asyncbodyand accepts an optionalzoneSpecification(e.g. to interceptprint).override/includeIfAbsentsurviveawaitinrunMergedScopedtoo — reach for this variant when you want the stricter async signature or needzoneSpecification, not because bindings would otherwise be lost.runScopedGuarded(body, {onError, values})— likerunScoped, but uncaught errors are routed toonErrorinstead of thrown.ref.overrideWith(() => other)— a ref with the same identity asrefbut 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.