armature_flutter 0.3.1 copy "armature_flutter: ^0.3.1" to clipboard
armature_flutter: ^0.3.1 copied to clipboard

Flutter integration for armature: ArmatureApp bootstrap, slot widgets, reactive providers, and a debug overlay.

armature_flutter #

pub package likes points CI

Flutter integration for armature — app bootstrap, slot widgets, reactive providers, and an interactive feature-graph debug overlay.

Install #

dependencies:
  armature: ^0.3.1
  armature_flutter: ^0.3.1

Quickstart #

Wrap your app with ArmatureApp, register your features, and mount a feature root:

import 'package:armature/armature.dart';
import 'package:armature_flutter/armature_flutter.dart';
import 'package:flutter/material.dart';

final counterFeature = createFeature(
  name: 'Counter',
  stores: (_) => (counter: CounterStore()),
  exports: (api) => api.own,
);

final counterRoot = createFeatureRoot(
  feature: counterFeature,
  widget: const CounterScreen(),
);

void main() {
  runApp(ArmatureApp(
    features: [counterFeature],
    child: counterRoot(data: null),
  ));
}

Reading store state in widgets #

Three tools, ordered from imperative to most granular:

context.store<T>() — one-shot #

For event handlers and imperative reads. Does not rebuild.

ElevatedButton(
  onPressed: () => context.store<CounterStore>().increment(),
  child: const Text('+'),
)

StoreBuilder<T> — reactive #

Rebuilds on any tracked .state / atom read inside the builder.

StoreBuilder<CounterStore>(
  builder: (context, store) => Text('${store.state}'),
)

StoreSelector<V> — equality-based #

Rebuilds only when the derived value changes. Great for deriving scalars or combining multiple stores into a record / view-model.

StoreSelector<({String name, int count})>(
  select: (ctx) => (
    name: ctx.store<UserStore>().state.name,
    count: ctx.store<CounterStore>().state,
  ),
  builder: (_, data) => Text('${data.name}: ${data.count}'),
)

Slots — composing UI across features #

Slots are ports that produce widgets. The owning feature declares the slot; child features contribute widgets via extensions.

SingleSlot — pick by priority #

// owner (layoutFeature) declares the slot at top level:
final titleSlot = createSingleSlot<LayoutMode>(name: 'layout.title');

final layoutFeature = createFeature(
  name: 'Layout',
  ports: (titleSlot: titleSlot),
  stores: (_) => (activeTab: ActiveTabStore()),
  exports: (api) => api.own,
);

// child feature contributes a widget:
authFeature.useSingleSlot(
  layoutFeature.ports.titleSlot,
  (mode, api) => Text('Hello, ${api.own.auth.state.user?.name}'),
  priority: 2,
);

// owner (or any descendant) renders the slot:
SingleSlotProvider(
  slot: layoutFeature.ports.titleSlot,
  data: LayoutMode.phone,
  builder: (child, _) => child ?? const Text('No title'),
)

KeyedSingleSlot / KeyedMultiSlot — same slot, indexed by string key #

A keyed slot is a family of slots — one per key — sharing a single declaration. Useful when a parent hosts several routes / tabs in the same conceptual position.

// owner declares the keyed slot:
final bodyKeyedSlot = createKeyedSingleSlot<LayoutMode>(name: 'layout.body');

final layoutFeature = createFeature(
  name: 'Layout',
  ports: (body: bodyKeyedSlot),
  stores: (_) => (activeTab: ActiveTabStore()),
  exports: (api) => api.own,
);

// each child feature contributes to a specific key:
counterFeature.useSingleSlot(
  layoutFeature.ports.body('counter'),
  (mode, api) => CounterTab(store: api.own.counter),
);

historyFeature.useSingleSlot(
  layoutFeature.ports.body('history'),
  (mode, api) => HistoryTab(store: api.own.history),
);

// render the active tab's slot:
SingleSlotProvider(
  slot: layoutFeature.ports.body(activeTab),
  data: LayoutMode.phone,
  builder: (child, _) => child ?? const Text('Empty tab'),
)

createKeyedMultiSlot is the same idea for MultiSlot (one list of widgets per key).

Other slot kinds and providers #

  • MultiSlot — collects all active widgets, sorts by order. Default orderDirection is MultiSlotOrderDirection.asc.

  • PipeProvider / BehaviorProvider — reactive providers for pipe / behavior ports.

  • MultiPortBuilder — reads any mix of ports in a single builder with fine-grained reactive tracking. Each reader.* call tracks exactly one port; rebuilds fire only for the ports that change:

    MultiPortBuilder(
      builder: (reader, _) {
        final tabs = reader.pipe(tabsPipe, initialValue: const <TabSpec>[]);
        final actions = reader.multi(actionsSlot, data: LayoutMode.phone);
        return Scaffold(
          appBar: AppBar(actions: actions),
          body: TabBar(tabs: [for (final t in tabs) Tab(text: t.label)]),
        );
      },
    )
    
  • StateObserver — raw reactive builder for arbitrary Atom / Store.state reads, no DI lookup. Use when you build custom widgets that touch reactive state outside the typed StoreBuilder / StoreSelector path.

Debug overlay #

Wrap your app with FeatureGraphOverlay to get an interactive feature-graph inspector (enable only in debug):

runApp(ArmatureApp(
  features: [...],
  child: FeatureGraphOverlay(
    enabled: kDebugMode,
    child: rootBuilder(data: null),
  ),
));

Features:

  • Pan / zoom / long-press-drag the DAG.
  • Tap a node → see dependencies, stores (live state), ports, handlers.
  • Refresh button — re-snapshots status + adds any features appearing post-start; preserves drag positions.
  • Live State Inspector tab — every active store's state, re-renders on change.

Testing #

package:armature_flutter/test_utils.dart:

import 'package:armature/test_utils.dart';
import 'package:armature_flutter/test_utils.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
  testWidgets('renders counter', (tester) async {
    final container = await startedContainer(features: [counterFeature]);
    // pumpFeature auto-installs a FlutterRenderer on the container if
    // none is set yet. For containers built outside ArmatureApp, call
    // initTestRenderer(container) explicitly only if you need a custom
    // FlutterRendererOptions.
    await pumpFeature(
      tester,
      container: container,
      feature: counterFeature,
      child: StoreBuilder<CounterStore>(
        builder: (_, s) => Text('${s.state}'),
      ),
    );
    expect(find.text('0'), findsOneWidget);
  });
}

Learn more #

License #

MIT — see LICENSE.

1
likes
0
points
406
downloads

Publisher

unverified uploader

Weekly Downloads

Flutter integration for armature: ArmatureApp bootstrap, slot widgets, reactive providers, and a debug overlay.

Homepage
Repository (GitHub)
View/report issues

Topics

#flutter #framework #state-management #architecture #feature-based

License

unknown (license)

Dependencies

armature, armature_reactive, flutter, flutter_test, meta

More

Packages that depend on armature_flutter