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.

example/lib/main.dart

// Comprehensive demo of every atomly_flutter feature.
//
// Run with:
//
//   cd packages/atomly_flutter/example
//   flutter run
//
// Sections (one tab each):
//   1. Counter        — sync atoms, computed, child rebuild isolation
//   2. Async user     — Atom.future + AtomAsyncBuilder + refresh
//   3. Family         — parameterized atoms keyed by id
//   4. Listener       — side effects (snackbar) without rebuilds
//   5. Override demo  — test overrides at the scope level

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

// ─────────────────────────────────────────────────────────────────────
// 1. Sync state + computed
// ─────────────────────────────────────────────────────────────────────

final counter = Atom(0);
final doubled = Atom.computed((read) => read(counter) * 2);
final isEven = Atom.computed((read) => read(counter) % 2 == 0);

// ─────────────────────────────────────────────────────────────────────
// 2. Async state with Atom.future
// ─────────────────────────────────────────────────────────────────────

class User {
  final String id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});
}

final selectedUserId = Atom('u_1');

// Simulated API. Fails roughly every fourth call so the error path
// shows up too.
int _userFetchCount = 0;
final user = Atom.future<User>((read) async {
  final id = read(selectedUserId);
  _userFetchCount++;
  await Future<void>.delayed(const Duration(milliseconds: 600));
  if (_userFetchCount % 4 == 0) {
    throw Exception('network blip');
  }
  return User(
    id: id,
    name: 'User $id',
    email: '$id@example.com',
  );
});

// ─────────────────────────────────────────────────────────────────────
// 3. Family — parameterized atoms
// ─────────────────────────────────────────────────────────────────────

final post = Atom.family<int, AtomValue<String>>(
  (id) => Atom.future((read) async {
    await Future<void>.delayed(const Duration(milliseconds: 400));
    return 'Post #$id — lorem ipsum dolor sit amet';
  }),
);

// ─────────────────────────────────────────────────────────────────────
// 5. Override demo — a separate counter that the scope below pins to 99
// ─────────────────────────────────────────────────────────────────────

final overriddenCounter = Atom(0);

// ─────────────────────────────────────────────────────────────────────
// App entry
// ─────────────────────────────────────────────────────────────────────

void main() {
  runApp(
    AtomScope(
      observers: [
        CallbackAtomObserver(
          onUpdate: (atom, prev, next) =>
              debugPrint('[atomly] $atom: $prev → $next'),
        ),
      ],
      child: const AtomlyDemoApp(),
    ),
  );
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'atomly demo',
      theme: ThemeData(useMaterial3: true, colorSchemeSeed: Colors.deepPurple),
      home: const _HomeShell(),
    );
  }
}

class _HomeShell extends StatelessWidget {
  const _HomeShell();

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 5,
      child: Scaffold(
        appBar: AppBar(
          title: const Text('atomly demo'),
          bottom: const TabBar(
            isScrollable: true,
            tabs: [
              Tab(text: '1 · Counter'),
              Tab(text: '2 · Async'),
              Tab(text: '3 · Family'),
              Tab(text: '4 · Listener'),
              Tab(text: '5 · Override'),
            ],
          ),
        ),
        body: const TabBarView(
          children: [
            _CounterPage(),
            _AsyncPage(),
            _FamilyPage(),
            _ListenerPage(),
            _OverridePage(),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────
// Tab 1 — Counter
// ─────────────────────────────────────────────────────────────────────

class _CounterPage extends StatelessWidget {
  const _CounterPage();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const _SectionTitle('Sync atoms + computed atoms'),
          const SizedBox(height: 16),
          // Each child watches a different atom — only the matching
          // child rebuilds when that atom changes.
          const _CounterReadout(),
          const SizedBox(height: 8),
          const _DoubledReadout(),
          const SizedBox(height: 8),
          const _IsEvenReadout(),
          const SizedBox(height: 24),
          Row(
            children: [
              Expanded(
                child: FilledButton.tonal(
                  onPressed: () => counter.update(context, (v) => v - 1),
                  child: const Text('−1'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: FilledButton(
                  onPressed: () => counter.update(context, (v) => v + 1),
                  child: const Text('+1'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          OutlinedButton(
            onPressed: () => counter.set(context, 0),
            child: const Text('reset'),
          ),
        ],
      ),
    );
  }
}

class _CounterReadout extends StatelessWidget {
  const _CounterReadout();
  @override
  Widget build(BuildContext context) {
    final value = counter.watch(context);
    return _Tile(label: 'counter', value: '$value');
  }
}

class _DoubledReadout extends StatelessWidget {
  const _DoubledReadout();
  @override
  Widget build(BuildContext context) {
    final value = doubled.watch(context);
    return _Tile(label: 'doubled (computed)', value: '$value');
  }
}

class _IsEvenReadout extends StatelessWidget {
  const _IsEvenReadout();
  @override
  Widget build(BuildContext context) {
    final v = isEven.watch(context);
    return _Tile(label: 'isEven (computed)', value: '$v');
  }
}

// ─────────────────────────────────────────────────────────────────────
// Tab 2 — Async
// ─────────────────────────────────────────────────────────────────────

class _AsyncPage extends StatelessWidget {
  const _AsyncPage();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const _SectionTitle('Atom.future + AtomAsyncBuilder + refresh'),
          const SizedBox(height: 16),
          const _UserCard(),
          const SizedBox(height: 24),
          Row(
            children: [
              Expanded(
                child: OutlinedButton(
                  onPressed: () => selectedUserId.set(context, 'u_1'),
                  child: const Text('Select u_1'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: () => selectedUserId.set(context, 'u_2'),
                  child: const Text('Select u_2'),
                ),
              ),
              const SizedBox(width: 12),
              Expanded(
                child: OutlinedButton(
                  onPressed: () => selectedUserId.set(context, 'u_3'),
                  child: const Text('Select u_3'),
                ),
              ),
            ],
          ),
          const SizedBox(height: 12),
          FilledButton.tonal(
            onPressed: () => user.refresh(context),
            child: const Text('Refresh'),
          ),
        ],
      ),
    );
  }
}

class _UserCard extends StatelessWidget {
  const _UserCard();

  @override
  Widget build(BuildContext context) {
    return Card(
      child: SizedBox(
        height: 120,
        child: Center(
          child: AtomAsyncBuilder<User>(
            atom: user,
            loading: (_) =>
                const Column(mainAxisSize: MainAxisSize.min, children: [
              CircularProgressIndicator(),
              SizedBox(height: 8),
              Text('Loading…'),
            ]),
            data: (_, u) => Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                Text(u.name,
                    style: Theme.of(context).textTheme.titleLarge),
                Text(u.email,
                    style: Theme.of(context).textTheme.bodySmall),
              ],
            ),
            error: (_, e, st) => Padding(
              padding: const EdgeInsets.all(12),
              child: Text(
                'Error: $e',
                style: TextStyle(color: Theme.of(context).colorScheme.error),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────
// Tab 3 — Family
// ─────────────────────────────────────────────────────────────────────

class _FamilyPage extends StatelessWidget {
  const _FamilyPage();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const _SectionTitle('Atom.family — parameterized atoms'),
          const SizedBox(height: 16),
          const Text(
            'Each tile below watches a separate post atom keyed by id. '
            'They load independently and dispose independently.',
          ),
          const SizedBox(height: 16),
          for (final id in [101, 102, 103])
            Padding(
              padding: const EdgeInsets.only(bottom: 8),
              child: _PostTile(id: id),
            ),
        ],
      ),
    );
  }
}

class _PostTile extends StatelessWidget {
  const _PostTile({required this.id});
  final int id;

  @override
  Widget build(BuildContext context) {
    return Card(
      child: ListTile(
        leading: CircleAvatar(child: Text('$id')),
        title: AtomAsyncBuilder<String>(
          atom: post(id),
          loading: (_) => const Text('loading…'),
          data: (_, body) => Text(body),
          error: (_, e, st) => Text('error: $e'),
        ),
        trailing: IconButton(
          icon: const Icon(Icons.refresh),
          onPressed: () => post(id).refresh(context),
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────
// Tab 4 — Listener (side effects without rebuilds)
// ─────────────────────────────────────────────────────────────────────

class _ListenerPage extends StatelessWidget {
  const _ListenerPage();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: AtomListener<AtomValue<User>>(
        atom: user,
        onChange: (ctx, prev, next) {
          if (next case AtomError(:final error)) {
            ScaffoldMessenger.of(ctx).showSnackBar(
              SnackBar(content: Text('user fetch failed: $error')),
            );
          } else if (next case AtomData(:final value)) {
            ScaffoldMessenger.of(ctx).showSnackBar(
              SnackBar(content: Text('loaded ${value.name}')),
            );
          }
        },
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const _SectionTitle('AtomListener — side effects'),
            const SizedBox(height: 16),
            const Text(
              'This page mounts an AtomListener over the user atom. '
              'Tap "Refresh user" — every change shows a snackbar without '
              'rebuilding the page. Some attempts will fail (the demo '
              'API throws roughly 1 in 4) so you can see both branches.',
            ),
            const SizedBox(height: 24),
            FilledButton(
              onPressed: () => user.refresh(context),
              child: const Text('Refresh user'),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────
// Tab 5 — Override (test-style scope override)
// ─────────────────────────────────────────────────────────────────────

class _OverridePage extends StatelessWidget {
  const _OverridePage();

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.stretch,
        children: [
          const _SectionTitle('Nested AtomScope with overrides'),
          const SizedBox(height: 16),
          const Text(
            'The inner scope below pins overriddenCounter to 99 — only '
            'inside its subtree. The outer scope still sees the real '
            'value (which starts at 0).',
          ),
          const SizedBox(height: 16),
          AtomBuilder<int>(
            atom: overriddenCounter,
            builder: (_, v, __) => _Tile(label: 'outer scope', value: '$v'),
          ),
          const SizedBox(height: 16),
          AtomScope(
            overrides: [overriddenCounter.overrideWithValue(99)],
            child: AtomBuilder<int>(
              atom: overriddenCounter,
              builder: (_, v, __) => _Tile(label: 'inner scope', value: '$v'),
            ),
          ),
          const SizedBox(height: 24),
          OutlinedButton(
            onPressed: () => overriddenCounter.update(context, (v) => v + 1),
            child: const Text('+1 (outer only)'),
          ),
        ],
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────
// Shared UI bits
// ─────────────────────────────────────────────────────────────────────

class _SectionTitle extends StatelessWidget {
  const _SectionTitle(this.text);
  final String text;
  @override
  Widget build(BuildContext context) {
    return Text(text, style: Theme.of(context).textTheme.titleMedium);
  }
}

class _Tile extends StatelessWidget {
  const _Tile({required this.label, required this.value});
  final String label;
  final String value;
  @override
  Widget build(BuildContext context) {
    return Card(
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
        child: Row(
          children: [
            Expanded(child: Text(label)),
            Text(value, style: Theme.of(context).textTheme.titleMedium),
          ],
        ),
      ),
    );
  }
}
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