AtomicFlutter v0.3.0: Lightweight Reactive State Management for Flutter

Zero dependencies. Maximum power. Minimal boilerplate.

The State Management Problem


If you've built Flutter apps, you know the struggle. State management solutions are either:

  • Too complex - Steep learning curves, massive boilerplate (looking at you, BLoC)
  • Too simple - Great for demos, but don't scale (setState anyone?)
  • Too opinionated - Force you into specific patterns
  • Too heavy - Pulling in huge dependency trees

What if there was a better way?


Introducing AtomicFlutter

AtomicFlutter is a lightweight, reactive state management library that brings fine-grained reactivity to Flutter. Think of it as Signals for Flutter, or Jotai for React developers.

Why "AtomicFlutter"?

Atoms are the smallest units of state. Each atom:

  • Manages a single piece of state
  • Notifies only the widgets that care
  • Automatically disposes when no longer needed
  • Composes beautifully with other atoms

No global stores. No reducers. No actions. Just simple, reactive state.

Quick Example

Here's a complete counter app in 23 lines of code:

import 'package:atomic_flutter/atomic_flutter.dart';

final counterAtom = Atom<int>(0);

class CounterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: AtomBuilder(
            atom: counterAtom,
            builder: (context, count) => Text('$count'),
          ),
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => counterAtom.update((c) => c + 1),
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

No providers. No context. No boilerplate. Just works.


Core Features

1. Fine-Grained Reactivity

Only rebuild what changed. AtomicFlutter tracks dependencies at the atom level, not the widget tree level.

final userAtom = Atom<User>(currentUser);
final nameAtom = computed(() => userAtom.value.name, tracked: [userAtom]);

// This widget only rebuilds when the NAME changes
AtomBuilder(
  atom: nameAtom,
  builder: (context, name) => Text(name),
)

2. Async State Made Easy

final productsAtom = AsyncAtom<List<Product>>();

// Execute async operation
await productsAtom.execute(() => api.getProducts());

// UI handles all states automatically
AsyncBuilder<List<Product>>(
  atom: productsAtom,
  loading: (context) => CircularProgressIndicator(),
  error: (context, error) => ErrorWidget(error),
  builder: (context, products) => ProductList(products),
)

3. Automatic Memory Management

Atoms dispose themselves when no longer used. No memory leaks, no manual cleanup (unless you want it).

// Auto-disposes after 2 minutes of no listeners
final tempAtom = Atom<String>('temp', autoDispose: true);

// Never disposes (global state)
final themeAtom = Atom<ThemeMode>(ThemeMode.light, autoDispose: false);

4. Powerful Extensions

Transform, filter, debounce, throttle - all built-in:

// Debounced search
final searchAtom = Atom<String>('');
final debouncedSearch = searchAtom.debounce(Duration(milliseconds: 300));

// Only even numbers
final numberAtom = Atom<int>(0);
final evenNumbers = numberAtom.where((n) => n % 2 == 0);

// Double the values
final doubled = numberAtom.map((n) => n * 2);

What's New in v0.3.0

A massive update has been shipped with critical bug fixes and quality improvements:

Critical Bug Fixes

Memory Leak Fixes - Fixed memory leaks in 11 extension methods:

  • All debouncethrottlemapwherecombine methods now properly cleanup
  • Bidirectional cleanup: both source and derived atoms dispose correctly
  • Timer cleanup prevents resource leaks

Before v0.3.0:

final debounced = searchAtom.debounce(Duration(seconds: 1));
// ❌ Memory leak - listener never removed!

After v0.3.0:

final debounced = searchAtom.debounce(Duration(seconds: 1));
// ✅ Properly cleans up when disposed
debounced.dispose(); // All listeners removed

Type Safety Improvements

Computed Atoms are Now Read-Only:

final total = computed(() => price.value * quantity.value, tracked: [price, quantity]);

total.set(999);
// ❌ Throws UnsupportedError with clear message
// "Cannot directly set value of computed atom. Update its dependencies instead."

Circular Dependency Detection:

final a = Atom<int>(0);
final b = computed(() => a.value + 1, tracked: [a]);
final c = computed(() => b.value + a.value, tracked: [a, b]); // ✅ Works fine

// But this throws:
final d = computed(() => c.value + 1, tracked: [c]);
final e = computed(() => d.value + 1, tracked: [d, e]); // ❌ Circular!
// StateError: Circular dependency detected

Error Isolation

Failing listeners no longer crash your app:

atom.addListener((value) {
  throw Exception('Oops!'); // ❌ Before: App crashes
});

atom.addListener((value) {
  updateUI(value); // ✅ After: This still runs!
});

Complete v0.3.0 Changes

  • Fixed memory leaks in 11 extension methods
  • Computed atoms are type-safe (can't mutate)
  • Circular dependency detection
  • Error isolation in listeners
  • StreamController proper disposal
  • Edge case fixes (empty lists, initial states)
  • 31+ new test cases
  • Enhanced documentation

Advanced Patterns

Domain-Specific Atoms

Encapsulate business logic in custom atoms:

class CartAtom extends Atom<Cart> {
  CartAtom() : super(const Cart(), autoDispose: false);

  void addProduct(Product product, int quantity) {
    update((cart) => cart.addItem(
      CartItem(product: product, quantity: quantity),
    ));
    _saveToBackend(); // Optimistic update!
  }

  void removeProduct(int productId) {
    update((cart) => cart.removeItem(productId));
    _saveToBackend();
  }

  Future<void> _saveToBackend() async {
    await api.saveCart(value);
  }
}

Async Retry Logic

Exponential backoff built-in:

final dataAtom = AsyncAtom<Data>();

await dataAtom.executeWithRetry(
  () => api.fetchData(),
  maxRetries: 3,
  delay: Duration(seconds: 1), // 1s, 2s, 3s delays
);

Optimistic Updates

Update UI immediately, sync later:

void likePost(Post post) {
  // Update UI instantly
  postsAtom.update((posts) =>
    posts.map((p) => p.id == post.id ? p.copyWith(liked: true) : p).toList()
  );

  // Sync with backend
  api.likePost(post.id).catchError((e) {
    // Rollback on error
    postsAtom.update((posts) =>
      posts.map((p) => p.id == post.id ? p.copyWith(liked: false) : p).toList()
    );
  });
}

Performance Optimization

Use AtomSelector to rebuild only when specific fields change:

// Rebuilds on EVERY cart change
AtomBuilder(
  atom: cartAtom,
  builder: (context, cart) => Text('\$${cart.totalPrice}'),
)

// ✅ Rebuilds ONLY when totalPrice changes
cartAtom.select<double>(
  selector: (cart) => cart.totalPrice,
  builder: (context, total) => Text('\$$total'),
)

Real-World Example: E-Commerce App

Here's how you'd structure a production app:

// Domain atoms
final productsAtom = AsyncAtom<List<Product>>();
final cartAtom = CartAtom();
final userAtom = Atom<User?>(null);

// Computed atoms
final cartCountAtom = computed<int>(
  () => cartAtom.value.itemCount,
  tracked: [cartAtom],
);

final cartTotalAtom = computed<double>(
  () => cartAtom.value.totalPrice,
  tracked: [cartAtom],
);

// Search with debouncing
final searchQueryAtom = Atom<String>('');
final debouncedSearchAtom = searchQueryAtom.debounce(
  Duration(milliseconds: 500),
);

final filteredProductsAtom = computed<List<Product>>(
  () {
    final products = productsAtom.value;
    final query = debouncedSearchAtom.value;

    if (!products.hasValue || query.isEmpty) return [];

    return products.value
      .where((p) => p.name.toLowerCase().contains(query.toLowerCase()))
      .toList();
  },
  tracked: [productsAtom, debouncedSearchAtom],
);

UI Layer:

class ProductsScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Products'),
        actions: [
          AtomBuilder(
            atom: cartCountAtom,
            builder: (context, count) => Badge(
              label: Text('$count'),
              child: Icon(Icons.shopping_cart),
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          // Search bar
          TextField(
            onChanged: searchQueryAtom.set,
            decoration: InputDecoration(
              hintText: 'Search (debounced 500ms)',
            ),
          ),

          // Products list
          Expanded(
            child: AsyncBuilder<List<Product>>(
              atom: productsAtom,
              enableRefresh: true,
              loading: (context) => CircularProgressIndicator(),
              error: (context, error) => ErrorRetryWidget(error),
              builder: (context, products) {
                return AtomBuilder(
                  atom: filteredProductsAtom,
                  builder: (context, filtered) => ProductList(filtered),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}

Comparison with Other Solutions

FeatureAtomicFlutterProviderRiverpodBLoCGetX
Dependencies0234+1
Learning Curve⭐ Easy⭐⭐ Medium⭐⭐⭐ Hard⭐⭐⭐⭐ Very Hard⭐⭐ Medium
BoilerplateMinimalMediumMediumHeavyMinimal
Fine-grained Updates✅ Yes❌ No✅ Yes❌ No⚠️ Partial
Async Built-in✅ Yes❌ No✅ Yes✅ Yes✅ Yes
Auto Disposal✅ Yes❌ No✅ Yes❌ No⚠️ Manual
Type Safety✅ Strong✅ Strong✅ Strong✅ Strong⚠️ Weak
DevToolsPlanned✅ Yes✅ Yes✅ Yes✅ Yes

Performance

AtomicFlutter is designed for efficiency. Here's how:

  1. Fine-grained reactivity - Only affected widgets rebuild, not entire subtrees
  2. Atom-level tracking - No context lookups, no widget tree traversal
  3. Efficient equality checks - Uses identical() first (fast path), then ==
  4. Lazy evaluation - Computed atoms only recompute when dependencies actually change
  5. Smart disposal - Reference counting prevents unnecessary work

The fine-grained reactivity means that in apps with complex state, you'll see fewer rebuilds and better performance compared to solutions that rebuild entire subtrees.


Migration Guide

From Provider

Before (Provider):

class CounterProvider extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

// In widget
Provider.of<CounterProvider>(context).increment();
Consumer<CounterProvider>(
  builder: (context, counter, child) => Text('${counter.count}'),
)

After (AtomicFlutter):

final counterAtom = Atom<int>(0);

// In widget
counterAtom.update((c) => c + 1);
AtomBuilder(
  atom: counterAtom,
  builder: (context, count) => Text('$count'),
)

From Riverpod

Before (Riverpod):

final counterProvider = StateProvider<int>((ref) => 0);

// In widget
final count = ref.watch(counterProvider);
ref.read(counterProvider.notifier).state++;

After (AtomicFlutter):

final counterAtom = Atom<int>(0);

// In widget
AtomBuilder(
  atom: counterAtom,
  builder: (context, count) => Text('$count'),
)
counterAtom.update((c) => c + 1);

When to Use AtomicFlutter

Perfect for:

  • Apps of any size (small to large)
  • Teams that want minimal boilerplate
  • Developers familiar with Signals/Jotai/Recoil
  • Projects that need fine-grained reactivity
  • Apps with complex async state
  • When you want automatic memory management

Consider alternatives if:

  • You need mature DevTools (coming soon!)
  • Your team is already invested in another solution
  • You need very specific patterns (like Redux time-travel)

Getting Started

Installation

dependencies:
  atomic_flutter: ^0.3.0

Basic Usage

import 'package:atomic_flutter/atomic_flutter.dart';

// 1. Create atoms
final nameAtom = Atom<String>('John');
final ageAtom = Atom<int>(25);

// 2. Computed atoms
final greetingAtom = computed<String>(
  () => 'Hello, ${nameAtom.value}! You are ${ageAtom.value} years old.',
  tracked: [nameAtom, ageAtom],
);

// 3. Use in widgets
AtomBuilder(
  atom: greetingAtom,
  builder: (context, greeting) => Text(greeting),
)

Async Example

final userAtom = AsyncAtom<User>();

// Load data
await userAtom.execute(() => api.getUser());

// Handle all states
AsyncBuilder<User>(
  atom: userAtom,
  loading: (context) => Spinner(),
  error: (context, error) => ErrorWidget(error),
  builder: (context, user) => UserProfile(user),
)

Documentation & Resources


What's Next?

We're working on:

  • DevTools Extension - Visual atom inspector
  • Performance Monitoring - Track atom updates and rebuilds
  • Time-Travel Debugging - Undo/redo state changes
  • Persistence Layer - Built-in local storage
  • Code Generation - Reduce boilerplate even more

Community

Join our growing community:


Conclusion

AtomicFlutter brings simplicity back to Flutter state management without sacrificing power. With v0.3.0, it's more robust than ever—zero memory leaks, type-safe, and production-ready.

Try it today:

flutter pub add atomic_flutter

Then build something amazing. 🚀