armature 0.3.1
armature: ^0.3.1 copied to clipboard
Feature-based app framework with dependency-graph resolution, reactive stores, typed ports (pipes / behaviors / slots), and tasks.
armature #
Feature-based application framework with dependency-graph resolution,
reactive stores, typed ports (Pipe / Behavior / slots), and tasks.
Pure Dart; pair with
armature_flutter for
Flutter UI.
Large applications quickly devolve into a web of providers and
singletons. armature solves this by giving each feature explicit
dependencies, eager store construction, and extension points
(ports) that other features plug into without mutual knowledge.
Install #
dependencies:
armature: ^0.3.1
armature_flutter: ^0.3.1 # if you want the Flutter integration
Quickstart #
Define a feature #
import 'package:armature/armature.dart';
typedef CounterState = ({int value});
class CounterStore extends Store<CounterState> {
CounterStore() : super(state: (value: 0));
void increment() => update((s) => (value: s.value + 1));
}
final counterFeature = createFeature(
name: "Counter",
stores: (_) => (counter: CounterStore()),
exports: (api) => api.own, // pass-through — children see { counter }
);
Declare dependencies #
final authFeature = createFeature(
name: "Auth",
stores: (_) => (auth: AuthStore()),
exports: (api) => api.own,
);
final adminFeature = createFeature(
name: "Admin",
dependsOn: [authFeature], // required parent
optionalDependsOn: [counterFeature], // optional — reachable via `api.of`
);
Activate + react #
adminFeature
..activation(whenActive(authFeature))
..onStart((api, cleanup) async {
final auth = api.of(authFeature).auth;
cleanup.subscribe(auth, (_, state) {
if (state.user?.name == 'admin') {
api.own.someStore.doWork();
}
});
});
Run the container #
final container = AppContainer(
features: [authFeature, counterFeature, adminFeature],
options: ContainerOptions(
errorHandler: ({required source, required error, required meta}) {
// source = feature name / '<container>' / '<events>'
logger.warn('[$source] $error');
},
),
);
await container.start();
// ...later:
await container.dispose();
Core concepts #
Features #
createFeature({name, dependsOn, optionalDependsOn, stores, exports, ports})— the sole constructor. Store / export factories are records-based:(counter: CounterStore(), repo: NotesStore()).- Lifecycle —
disabled→pending→active→ back todisabled. Stores are constructed eagerly duringstart(); onlyonStartreruns on activation cycles. - Activation helpers —
manualActivation,whenActive(parent),whenInactive(parent),whenAllActive([...]),whenStoreState(...).
Stores #
Store<T> wraps reactive state with listeners, async tasks, and
structural integration into the feature's scopeApi.
class AuthStore extends Store<({User? user})> {
AuthStore() : super(state: (user: null));
late final login = createTask(
fn: (String name) async {
await Future.delayed(const Duration(milliseconds: 200));
update((_) => (user: (name: name)));
},
);
void logout() => update((_) => (user: null));
}
Ports #
Extension points that other features plug into. Three kinds in
armature core:
Pipe<T>— sequential transformation. Each active handler receives the previous value, returns the next.Behavior<TBranch, TPayload>— priority-based selection. Active handlers returnBehaviorDescriptor(...); highest priority wins.- Slots (
SingleSlot/MultiSlot) — Flutter widgets; live inarmature_flutter.
// In owner's ports record:
final themeBehavior = createBehavior<ThemeMode, ThemeData>(name: 'theme');
// In child feature:
nightFeature.useBehavior(layoutFeature.ports.themeBehavior, (api) {
if (!api.own.night.state.enabled) return null;
return (branch: ThemeMode.dark, payload: ThemeData.dark());
}, priority: 10);
Tasks #
Strategy-backed async operations. strategy: is optional —
Store.createTask / createVoidTask default to .queue.
.queue(default) — FIFO sequential queue..once— blocks concurrent invocations until done..latest— only the most recent input finishes..debounce(duration)— fires once after quiet period..throttle(duration, edge)— rate-limit with leading / trailing edge control.
Lifecycle: task.reset() returns the state to TaskIdle (cancels
coalesced callers from .latest / .debounce / .throttle(trailing)
with TaskError, drops state writes from any in-flight run, clears
the .once cache). Pass autoReset: duration at creation to schedule
the same transition automatically after TaskDone / TaskFailed.
Picking TError — the third generic decides which thrown values
land in TaskFailed (sticky, observable in UI) vs propagate from
await task(...) and revert state to TaskIdle:
TError = |
Behaviour | When to use |
|---|---|---|
Exception |
Exception subclasses stick; Error propagates |
Default for API / IO. Domain failures show in UI; programming bugs surface to the error handler. |
domain class (e.g. ApiError) |
Only that family sticks | Strongly typed errors; UI pattern-matches on specific cases. |
Never |
Nothing sticks | Task isn't expected to throw in normal flow — any throw is a bug. |
Object |
Everything sticks, including bugs | Last resort. Loses the bug-vs-domain distinction. |
// Default for API/IO calls.
late final fetchUser = createTask<int, User, Exception>(
fn: (id) async => api.getUser(id),
);
// Typed exception hierarchy.
late final placeOrder = createTask<OrderRequest, OrderId, OrderError>(
fn: (req) async => api.placeOrder(req),
);
// Strict — fn shouldn't fail; any throw escalates to the caller.
late final increment = createVoidTask<int, Never>(
fn: () async => state.value + 1,
);
Error routing #
Everything user-actionable reaches ContainerOptions.errorHandler:
| What | Error type | source |
|---|---|---|
onStart / activation / handler throw |
HandlerError |
feature name |
Listener throw on featureStatusChanged |
ListenerError |
feature name |
Listener throw on portChanged |
ListenerError |
'<events>' |
| Port mis-scoped apply | PortError |
feature name |
| Slot widget build throw | RenderError |
feature name |
onDispose callback throw |
HandlerError |
'<container>' |
Advanced surface #
package:armature/armature.dart exposes the ~25 symbols you need to
author features, stores, tasks, and ports. Framework plumbing —
handler / listener typedefs (BehaviorHandler, PipeHandler,
TaskFn, StateChangeListener, ...), port base classes (Port,
AnyPort, PortType, PortSubscription), individual
TaskStrategy* constructor classes, debug-overlay mirrors, and
LoggerDebugInfo — lives in a separate barrel:
import 'package:armature/advanced.dart';
Reach for it when you need to type-annotate a handler field, build a custom debug overlay, or extend the framework. Day-to-day feature / store code should not need this import.
Learn more #
armature_flutter— Flutter integration:ArmatureApp, slot widgets, providers, debug overlay.armature_reactive— underlying reactive primitives.armature_graph— DAG resolver used for dependency graph.- Monorepo README — full architecture reference with extended examples.
License #
MIT — see LICENSE.