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: ^1.0.0
armature_flutter: ^1.0.0 # if you want the Flutter integration
Quickstart
A self-contained Pure Dart "hello world" with two features composing through a dependency. The Logger feature subscribes to the Counter feature's store and prints every change.
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,
);
// A second feature observes the counter through the dependency edge.
final loggerFeature = createFeature(
name: 'Logger',
dependsOn: [counterFeature],
)..onStart((api, cleanup) {
final counter = api.of(counterFeature).counter;
cleanup.subscribe(
counter,
(_, s) => print('counter: ${s.value}'),
fireImmediately: true,
);
counter.increment();
counter.increment();
});
Future<void> main() async {
final container = AppContainer(
features: [counterFeature, loggerFeature],
);
await container.start();
// Logger prints: counter: 0, counter: 1, counter: 2
await container.stop();
}
For the Flutter integration (ArmatureApp, slot widgets, StoreBuilder/StoreSelector, debug overlay), see armature_flutter.
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.
typedef NotesState = ({List<String> items});
class NotesStore extends Store<NotesState> {
NotesStore() : super(state: (items: const []));
void add(String text) => update((s) => (items: [...s.items, text]));
late final persist = createVoidTask(
fn: () async => await db.write(state.items),
strategy: TaskStrategy.debounce(Duration(milliseconds: 300)),
);
}
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 |
Advanced surface
package:armature/armature.dart exposes the ~25 symbols you need to author features, stores, tasks, and ports. Two extra barrels exist for narrower needs:
import 'package:armature/advanced.dart'; // application-level escape hatch
import 'package:armature/framework.dart'; // sibling-package plumbing
advanced.dart— handler / listener typedefs (BehaviorHandler,PipeHandler,TaskFn,StateChangeListener, ...), individualTaskStrategy*constructor classes, debug-overlay mirrors (ContainerDebug,FeatureDebugInfo,PortDebugInfo), andLoggerDebugInfo. Reach for it when you need to type-annotate a handler field, build a custom debug overlay, or implement a customLogger.framework.dart— base port hierarchy (Port,AnyPort,PortType,PortSubscription). This is plumbing thatarmature_flutterand customRendererimplementations need; application code should not import it — typed APIs (Pipe,Behavior, slot widgets) cover every end-user scenario.
Day-to-day feature / store code only needs armature.dart.
Learn more
- 📖 Documentation site — full guide with mental model, glossary, and a Notes/Todo example that builds out every concept.
armature_flutter— Flutter integration:ArmatureApp, slot widgets, providers, debug overlay.armature_reactive— underlying reactive primitives.armature_graph— DAG resolver used for dependency graph.- examples/armature_example — multi-feature reference app.
License
MIT — see LICENSE.
Libraries
- advanced
- Advanced / framework-adjacent surface of
package:armature. - armature
- Core public surface of
package:armature. - framework
- Framework-internal barrel for sibling packages (
armature_flutter, customRendererimplementations, debug tooling). Application code must not import this — the typed APIs inpackage:armature/armature.dartcover every end-user scenario. - test_utils
- Test utilities for code built on top of
armature.