blackbox 0.7.1
blackbox: ^0.7.1 copied to clipboard
Deterministic reactive computation core with explicit dependency graphs, boxes, and flows. Designed for testable business logic and state pipelines.
blackbox #
Reactive state management core for Dart.
Boxes hold state. Graphs wire dependencies. No code generation. No boilerplate.
See the full documentation with examples: README
Quick Start #
import 'package:blackbox/blackbox.dart';
class CounterBox extends NoInputBox<int> {
int _value = 0;
@override
int compute(int? previous) => _value;
void inc() => action(() => _value++);
}
API #
Box Types #
| Class | Input | Sync/Async |
|---|---|---|
NoInputBox<O> |
none | sync |
Box<I, O> |
yes | sync |
NoInputAsyncBox<O> |
none | async |
AsyncBox<I, O> |
yes | async |
Graph #
final graph = Graph.builder()
.add(boxA)
.add(boxB, input: (d) => d.whenReady(boxA))
.build(start: true);
Effects #
Graph.builder()
.add(checkoutState)
.addEffect<CheckoutState>(
(d) => d.whenReady(checkoutState),
run: (current, previous) {
if (previous is! CheckoutSuccess && current is CheckoutSuccess) {
cart.clear();
}
},
)
.build(start: true);
Effects are explicit graph sinks:
- they run only when their input changes
- they receive
currentandprevious - async handlers are fire-and-forget
Persistence #
Add a mixin to any box — implement persistKeyFor() and you're done:
// Sync box
class ThemeBox extends NoInputBox<String> with Persisted<void, String> {
@override
String persistKeyFor(void _) => 'theme';
// ...
}
// Async box with persistence only
class UserBox extends AsyncBox<String, User>
with AsyncPersisted<String, User> {
@override
String persistKeyFor(String id) => 'user:$id';
// ...
}
// Async box with persistence + managed cache
class CachedUserBox extends AsyncBox<String, User>
with AsyncPersisted<String, User>, AsyncManagedCache<String, User> {
@override
String persistKeyFor(String id) => 'user:$id';
@override
Duration get cacheTtl => Duration(minutes: 5);
// ...
}
// Sync box with async fetch (always-available value, background refresh)
class StopListBox extends Box<String, StopList>
with ManagedCache<String, StopList> {
StopListBox(String input) : super(input, initialValue: StopList.empty);
@override
Duration get cacheTtl => Duration(seconds: 60);
@override
Future<StopList> fetch(String input) => api.getStopList(input);
}
Initialize persistence once before creating persistent boxes:
BlackboxPersistence.init(store, codecs: [UserJsonCodec()]);
Built-in codecs exist for int, double, String, and bool.
| Mixin | For | Features |
|---|---|---|
Persisted<I, O> |
Box, NoInputBox |
save/restore |
AsyncPersisted<I, O> |
AsyncBox, NoInputAsyncBox |
save/restore |
ManagedCache<I, O> |
on Box |
sync value + background fetch, TTL, fail-open, refresh(), invalidateCache() |
AsyncManagedCache<I, O> |
on AsyncBox, NoInputAsyncBox |
TTL, stale-while-refresh, refresh(), invalidateCache() (works standalone or composed with AsyncPersisted) |
Both cache mixins dedupe concurrent refresh() calls per box instance. Compose with Persisted / AsyncPersisted to persist cached values across restarts — the disk value wins over initialValue on boot.
In Flutter and Jaspr apps, prefer the platform adapters:
await SharedPrefsStore.preload()fromblackbox_flutterawait LocalStorageStore.preload()fromblackbox_jaspr
Lifecycle #
resolveInitialValue(I input, O? initialValue)— seed the initial state before the first computeonFirstCompute(I input, O? previous)— called once before the first computebeforeCompute(I input, O? previous)— optionally short-circuitcompute()with a readyFutureonReady()— called after initialization, before the first recomputedispose()— called bygraph.dispose()action(() { ... })— mutate state and trigger recomputationawait action(() async { ... })— for async boxes, completes after the recompute finishes
License #
MIT