armature 0.3.1 copy "armature: ^0.3.1" to clipboard
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 #

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: ^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()).
  • 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.

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 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
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 #

License #

MIT — see LICENSE.

1
likes
0
points
386
downloads

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

unknown (license)

Dependencies

armature_graph, armature_reactive, meta, test

More

Packages that depend on armature