armature 1.0.0 copy "armature: ^1.0.0" to clipboard
armature: ^1.0.0 copied to clipboard

Feature-based app framework with dependency-graph resolution, reactive stores, typed ports (pipes / behaviors / slots), and tasks.

armature #

pub package likes points CI

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()).
  • Lifecycledisabledpendingactive → back to disabled. Stores are constructed eagerly during start(); only onStart reruns on activation cycles.
  • Activation helpersmanualActivation, 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 return BehaviorDescriptor(...); highest priority wins.
  • Slots (SingleSlot / MultiSlot) — Flutter widgets; live in armature_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, ...), individual TaskStrategy* constructor classes, debug-overlay mirrors (ContainerDebug, FeatureDebugInfo, PortDebugInfo), and LoggerDebugInfo. Reach for it when you need to type-annotate a handler field, build a custom debug overlay, or implement a custom Logger.
  • framework.dart — base port hierarchy (Port, AnyPort, PortType, PortSubscription). This is plumbing that armature_flutter and custom Renderer implementations 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 #

License #

MIT — see LICENSE.

1
likes
160
points
399
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Feature-based app framework with dependency-graph resolution, reactive stores, typed ports (pipes / behaviors / slots), and tasks.

Homepage
Repository (GitHub)
View/report issues

Topics

#framework #architecture #state-management #feature-based #dependency-injection

License

MIT (license)

Dependencies

armature_graph, armature_reactive, meta, test

More

Packages that depend on armature