atomly_flutter 0.1.1 copy "atomly_flutter: ^0.1.1" to clipboard
atomly_flutter: ^0.1.1 copied to clipboard

Flutter integration for atomly. Provides AtomScope, AtomBuilder, AtomConsumer, AtomListener, and BuildContext extensions for reactive atomic state.

atomly_flutter #

Flutter integration for the atomly atomic state manager.

This package adds Flutter widgets and BuildContext extensions on top of the pure-Dart atomly core. The combined surface is what you typically import in a Flutter app:

import 'package:atomly_flutter/atomly_flutter.dart';
// re-exports everything from package:atomly/atomly.dart as well

Install #

dependencies:
  atomly: ^0.1.0
  atomly_flutter: ^0.1.0

What's in the box #

Symbol Purpose
AtomScope The runtime root — wraps your app, owns an AtomStore, exposes it via InheritedModel<Atom> for per-aspect rebuilds. Optionally takes overrides and observers.
BuildContext extensions on Atom<T> watch, read, select, set, update, refresh, invalidate. Plus when for Atom<AtomValue<T>>.
AtomBuilder<T> Single-atom builder widget for places where you can't easily call atom.watch(context). Supports a static child:.
AtomAsyncBuilder<T> Same shape as AtomBuilder, but pattern-matches data / loading / error for Atom<AtomValue<T>>.
AtomConsumer Multi-atom builder. The builder callback receives a watch function.
AtomListener<T> Side-effect-only widget. Runs onChange(context, previous, next) on every change without rebuilding the child.

Quick start #

import 'package:atomly_flutter/atomly_flutter.dart';
import 'package:flutter/material.dart';

final counter = Atom(0);

void main() {
  runApp(const AtomScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(child: CounterText()),
        floatingActionButton: FloatingActionButton(
          onPressed: () => counter.update(context, (v) => v + 1),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}

class CounterText extends StatelessWidget {
  const CounterText({super.key});

  @override
  Widget build(BuildContext context) {
    return Text(
      '${counter.watch(context)}',
      style: const TextStyle(fontSize: 64),
    );
  }
}

That's a complete Flutter counter app. No MultiBlocProvider, no ProviderScope types, no event/state classes. One AtomScope at the root, one Atom(0) at the top of the file, one counter.watch(context) in the widget. Done.

BuildContext extensions #

Inside any widget below an AtomScope:

// Read + subscribe (use in build)
final value = counter.watch(context);

// Read once (use in callbacks — no rebuild)
final current = counter.read(context);

// Write
counter.set(context, 42);
counter.update(context, (v) => v + 1);

// Re-run (for Atom.future / Atom.stream)
user.refresh(context);
user.invalidate(context); // drop cache and recreate

// Async pattern matching
user.when(
  context,
  data: (u) => Text(u.name),
  loading: () => const CircularProgressIndicator(),
  error: (e, st) => Text('$e'),
  refreshing: (previous) => Text('${previous.name} (refreshing…)'), // optional
);

Side effects with AtomListener #

AtomListener<T> is the right tool for snackbars, navigation, dialogs, analytics, haptics — anything you do not want firing twice because a parent widget rebuilt:

AtomListener<AtomValue<User>>(
  atom: user,
  onChange: (context, previous, next) {
    if (next case AtomError(:final error)) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('$error')),
      );
    }
  },
  child: const HomeBody(),
);

The child does not rebuild when the atom changes. The listener does not fire on initial mount — only on actual changes. Mount it once near the top of the screen and forget about it.

AtomBuilder and AtomAsyncBuilder #

When you cannot easily call atom.watch(context) from a widget's build (e.g., deep inside a third-party widget that takes a WidgetBuilder), use the builder widgets:

AtomBuilder<int>(
  atom: counter,
  builder: (context, value, child) => Text('$value'),
)

AtomAsyncBuilder<User>(
  atom: user,
  loading: (context) => const CircularProgressIndicator(),
  data: (context, u) => Text(u.name),
  error: (context, e, st) => Text('$e'),
  refreshing: (context, prev) => Text('${prev.name}…'), // optional
)

Both accept an optional child: argument that is not rebuilt when the atom changes — useful for performance when part of the subtree is static.

AtomConsumer — multi-atom builder #

Watch many atoms in one place without nesting builders:

AtomConsumer(
  builder: (context, watch) {
    final count = watch(counter);
    final doubled = watch(doubledAtom);
    final user = watch(currentUser);
    return Text('$count → $doubled (${user.id})');
  },
)

Test overrides #

AtomScope accepts an overrides: list. Build entries with atom.overrideWith(...) (substitute the atom) or atom.overrideWithValue(...) (pin to a fixed value):

testWidgets('shows test user', (tester) async {
  await tester.pumpWidget(
    AtomScope(
      overrides: [
        user.overrideWith(Atom.constant(AtomValue.data(testUser))),
        counter.overrideWithValue(42),
      ],
      child: const UserGreeting(),
    ),
  );
  expect(find.text('Test User'), findsOneWidget);
});

For headless unit tests (no widgets), use AtomStore directly — see the atomly package README.

Observers #

AtomScope(
  observers: [
    CallbackAtomObserver(
      onUpdate: (atom, prev, next) => debugPrint('$atom: $prev → $next'),
    ),
  ],
  child: const MyApp(),
);

Per-aspect rebuilds #

AtomScope is built on InheritedModel<Atom>. Every atom.watch(context) call registers that atom as the rebuild aspect — so widgets that watched only counter do not rebuild when userId changes, even though both atoms live in the same store. The filter is built in, not opt-in. There is no BlocSelector analogue to remember to use.

Example app #

A complete demo app exercising every feature lives at example/lib/main.dart. Five tabs:

  1. Counter — sync atoms, computed atoms, child rebuild isolation
  2. AsyncAtom.future + AtomAsyncBuilder + refresh
  3. Family — parameterized atoms keyed by id
  4. ListenerAtomListener showing snackbars without rebuilds
  5. Override — nested AtomScope with a scoped override

Run it:

cd packages/atomly_flutter/example
flutter pub get
flutter run

Tests #

This package ships with 18 widget tests covering AtomScope setup, scope lookup errors, override application, per-aspect rebuild filtering, AtomBuilder / AtomAsyncBuilder, AtomConsumer, AtomListener (no-rebuild guarantee), and the BuildContext extensions.

cd packages/atomly_flutter
flutter test

Public API reference #

package:atomly_flutter/atomly_flutter.dart exports:

  • Everything from package:atomly/atomly.dart (Atom, AtomValue, AtomStore, AtomReader, AtomOverride, AtomObserver, etc.)
  • AtomScope, AtomBuilder, AtomAsyncBuilder, AtomConsumer, AtomListener
  • BuildContext extensions: watch, read, select, set, update, refresh, invalidate, when

So you only need a single import in your Flutter app.

License #

MIT.

0
likes
160
points
33
downloads

Documentation

API reference

Publisher

verified publisherzakriakhan.com

Weekly Downloads

Flutter integration for atomly. Provides AtomScope, AtomBuilder, AtomConsumer, AtomListener, and BuildContext extensions for reactive atomic state.

Homepage
Repository (GitHub)
View/report issues

Topics

#state #state-management #flutter #reactive #atom

License

MIT (license)

Dependencies

atomly, flutter, meta

More

Packages that depend on atomly_flutter