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
debounce,throttle,map,where,combinemethods 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 removedType 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
| Feature | AtomicFlutter | Provider | Riverpod | BLoC | GetX |
|---|---|---|---|---|---|
| Dependencies | 0 | 2 | 3 | 4+ | 1 |
| Learning Curve | ⭐ Easy | ⭐⭐ Medium | ⭐⭐⭐ Hard | ⭐⭐⭐⭐ Very Hard | ⭐⭐ Medium |
| Boilerplate | Minimal | Medium | Medium | Heavy | Minimal |
| 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 |
| DevTools | Planned | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
Performance
AtomicFlutter is designed for efficiency. Here's how:
- Fine-grained reactivity - Only affected widgets rebuild, not entire subtrees
- Atom-level tracking - No context lookups, no widget tree traversal
- Efficient equality checks - Uses
identical()first (fast path), then== - Lazy evaluation - Computed atoms only recompute when dependencies actually change
- 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
- Documentation: pub.dev/packages/atomic_flutter
- GitHub: github.com/arkarmintun1/atomic_flutter
- Examples: Complete e-commerce app in
/example - Tests: 100+ test cases for reliability
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:
- Discord: [Link]
- Twitter: [@atomic_flutter_]
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. 🚀