atomic_flutter 0.1.3
atomic_flutter: ^0.1.3 copied to clipboard
A lightweight, reactive state management solution for Flutter applications with domain-specific atoms.
atomic_flutter #
AtomicFlutter is a lightweight, reactive state management solution for Flutter applications. It provides a simple way to create, manage, and react to state changes with minimal boilerplate and maximum type safety.
Table of Contents #
- atomic_flutter
Installation #
Add AtomicFlutter to your pubspec.yaml
:
dependencies:
atomic_flutter: ^0.1.3
Then import it in your Dart files:
import 'package:atomic_flutter/atomic_flutter.dart';
Core Concepts #
AtomicFlutter is based on the concept of atoms - individual units of state that can be observed and updated. It uses a reactive programming model where UI components subscribe to atoms and automatically rebuild when the atom's value changes.
Key Features #
- 🔄 Reactive State Management: Automatically update UI when state changes
- 🧩 Composable Atoms: Build complex state from simple primitives
- 🧠 Domain-Driven Design: Create domain-specific atoms with integrated business logic
- 🚮 Automatic Memory Management: Dispose of unused state to prevent memory leaks
- 📦 Minimal Dependencies: No external libraries required
- 🔍 Type Safety: Full type safety with no string-based lookups
- 🔧 Extensible: Easy to extend with custom functionality
- 🧲 Debugging Support: Track atom changes and history
Basic Usage #
Creating Atoms #
An atom is a container for a piece of state:
// Create an atom with an initial value
final counterAtom = Atom<int>(0);
// Atom with an ID (useful for debugging)
final nameAtom = Atom<String>('', id: 'nameAtom');
// Atom with custom auto-disposal settings
final settingsAtom = Atom<Map<String, dynamic>>(
{},
autoDispose: true,
disposeTimeout: Duration(minutes: 5),
);
Reading and Updating Atoms #
// Read the current value
int count = counterAtom.value;
// Update the value directly
counterAtom.set(5);
// Update based on the current value
counterAtom.update((current) => current + 1);
// Batch multiple updates to prevent intermediate rebuilds
counterAtom.batch(() {
counterAtom.set(0);
nameAtom.set('New User');
});
Widgets #
AtomicFlutter provides several widgets for efficiently connecting your UI to atoms:
AtomBuilder #
Rebuilds only when the atom's value changes:
AtomBuilder<int>(
atom: counterAtom,
builder: (context, count) {
return Text('Count: $count');
},
);
MultiAtomBuilder #
Rebuilds when any of the specified atoms change:
MultiAtomBuilder(
atoms: [userAtom, themeAtom],
builder: (context) {
final user = userAtom.value;
final theme = themeAtom.value;
return Text('Hello ${user.name}', style: theme.textStyle);
},
);
AtomSelector #
Rebuilds only when a selected part of an atom changes:
AtomSelector<UserProfile, String>(
atom: userProfileAtom,
selector: (profile) => profile.name,
builder: (context, name) {
return Text('Name: $name');
},
);
Derived State #
You can create atoms that derive their value from other atoms using the computed
function:
// Define primary atoms
final priceAtom = Atom<double>(10.0);
final quantityAtom = Atom<int>(2);
// Create a computed atom that depends on the other atoms
final totalAtom = computed<double>(
() => priceAtom.value * quantityAtom.value,
tracked: [priceAtom, quantityAtom],
id: 'totalPrice',
);
// totalAtom.value will automatically update when either price or quantity changes
Domain-Specific Atoms #
For cleaner code, extend Atom
to create domain-specific atoms:
class CartAtom extends Atom<Cart> {
CartAtom() : super(const Cart(), id: 'cart', autoDispose: false);
void addProduct(Product product, int quantity) {
update((cart) => cart.addItem(
CartItem(product: product, quantity: quantity)
));
}
void removeProduct(int productId) {
update((cart) => cart.removeItem(productId));
}
bool hasProduct(int productId) {
return value.items.any((item) => item.product.id == productId);
}
}
// Usage
final cartAtom = CartAtom();
cartAtom.addProduct(product, 2);
Extensions #
AtomicFlutter provides several useful extensions on the Atom class:
effect #
Execute side effects when an atom changes:
// Run a function whenever the atom changes
final cleanup = userAtom.effect((user) {
analytics.logUserChanged(user);
});
// Later, call the returned function to stop the effect
cleanup();
asStream #
Convert an atom to a Stream:
// Get a stream of the atom's values
Stream<User> userStream = userAtom.asStream();
// Use with StreamBuilder or other stream-based APIs
StreamBuilder<User>(
stream: userStream,
builder: (context, snapshot) {
// ...
},
);
select #
Shorthand for creating an AtomSelector:
userAtom.select(
selector: (user) => user.name,
builder: (context, name) {
return Text(name);
},
);
debounce #
Create a new atom that updates only after a delay:
// Create a search term atom
final searchTermAtom = Atom<String>('');
// Create a debounced version that only updates after 300ms of inactivity
final debouncedSearchAtom = searchTermAtom.debounce(Duration(milliseconds: 300));
// Use the debounced atom for API calls to avoid too many requests
searchTermAtom.effect((term) {
// This will run on every keystroke
print('Typing: $term');
});
debouncedSearchAtom.effect((term) {
// This will only run after typing stops for 300ms
api.search(term);
});
throttle #
Create a new atom that updates at most once per time period:
// Create a throttled atom that updates at most once per second
final throttledPositionAtom = positionAtom.throttle(Duration(seconds: 1));
Memory Management #
AtomicFlutter includes an automatic memory management system:
- Reference counting of atom usage
- Automatic disposal of unused atoms
- Configurable disposal timeouts
- Manual disposal when needed
// Global atom that never disposes
final themeAtom = Atom<ThemeMode>(ThemeMode.system, autoDispose: false);
// Screen-specific atom that auto-disposes after 2 minutes of disuse
final searchAtom = Atom<String>('', autoDispose: true);
// Custom disposal timeout
final cacheAtom = Atom<Map<String, dynamic>>(
{},
autoDispose: true,
disposeTimeout: Duration(minutes: 30),
);
// Register disposal callback
cacheAtom.onDispose(() {
// Cleanup code here
});
// Manually dispose
void cleanup() {
cacheAtom.dispose();
}
Options #
- Use
autoDispose: true
for atoms that should be cleaned up when no longer used - Set appropriate
disposeTimeout
values based on your app's needs - Call
dispose()
explicitly for atoms that you want to immediately clean up
Best Practices #
Atom Organization #
Organize your atoms based on feature or domain:
// User feature atoms
final userAtom = Atom<User?>(null, id: 'user');
final isAuthenticatedAtom = computed<bool>(
() => userAtom.value != null,
tracked: [userAtom],
);
// Cart feature atoms
final cartItemsAtom = Atom<List<CartItem>>([], id: 'cartItems');
final cartTotalAtom = computed<double>(
() => cartItemsAtom.value.fold(
0,
(total, item) => total + item.price * item.quantity,
),
tracked: [cartItemsAtom],
);
Debugging #
AtomicFlutter includes built-in debugging tools:
// Enable debug logging
enableDebugMode();
// Set the default timeout for auto-disposal
setDefaultDisposeTimeout(Duration(minutes: 1));
// Print debug information about all atoms
AtomDebugger.printAtomInfo();
Advanced Patterns #
Time-Travel Debugging #
Implement undo/redo functionality by tracking atom history:
class TimeTravel<T> {
final Atom<T> atom;
final Atom<List<T>> historyAtom;
final Atom<int> positionAtom;
TimeTravel({required this.atom, required String id})
: historyAtom = Atom<List<T>>([atom.value], id: '${id}_history'),
positionAtom = Atom<int>(0, id: '${id}_position') {
// Record history when atom changes
atom.addListener(_recordHistory);
}
void _recordHistory(T value) {
// Implementation details here
}
// Undo/redo methods
void undo() { /* ... */ }
void redo() { /* ... */ }
}
Persistent State #
Save atom values to persistent storage:
// Create a persistent atom
class PersistentAtom<T> {
final Atom<T> atom;
final String key;
final Future<void> Function(String, T) saveFunction;
final Future<T?> Function(String) loadFunction;
PersistentAtom({
required T initial,
required this.key,
required this.saveFunction,
required this.loadFunction,
String? id,
}) : atom = Atom<T>(initial, id: id) {
// Load initial value
loadFunction(key).then((value) {
if (value != null) {
atom.set(value);
}
});
// Save when value changes
atom.addListener((value) {
saveFunction(key, value);
});
}
T get value => atom.value;
void set(T value) => atom.set(value);
void update(T Function(T current) updater) => atom.update(updater);
}
Form Validation #
Implement form validation using computed atoms:
// Form field atoms
final emailAtom = Atom<String>('');
final passwordAtom = Atom<String>('');
// Validation atoms
final emailErrorAtom = computed<String?>(
() {
final email = emailAtom.value;
if (email.isEmpty) return 'Email is required';
if (!email.contains('@')) return 'Invalid email format';
return null;
},
tracked: [emailAtom],
);
final passwordErrorAtom = computed<String?>(
() {
final password = passwordAtom.value;
if (password.isEmpty) return 'Password is required';
if (password.length < 6) return 'Password must be at least 6 characters';
return null;
},
tracked: [passwordAtom],
);
// Overall form validity
final formValidAtom = computed<bool>(
() {
return emailErrorAtom.value == null &&
passwordErrorAtom.value == null &&
emailAtom.value.isNotEmpty &&
passwordAtom.value.isNotEmpty;
},
tracked: [emailErrorAtom, passwordErrorAtom, emailAtom, passwordAtom],
);
Authentication Flow #
Implement a typical authentication flow:
// Auth state enum
enum AuthState { initial, loading, authenticated, unauthenticated, error }
// Auth atoms
final authStateAtom = Atom<AuthState>(AuthState.initial);
final currentUserAtom = Atom<User?>(null);
final authErrorAtom = Atom<String?>(null);
// Auth controller
class AuthController {
Future<void> login(String email, String password) async {
try {
// Set loading state
authStateAtom.set(AuthState.loading);
authErrorAtom.set(null);
// Attempt login
final user = await authService.login(email, password);
// Update atoms with batch to avoid multiple rebuilds
authStateAtom.batch(() {
currentUserAtom.set(user);
authStateAtom.set(AuthState.authenticated);
});
} catch (e) {
// Update error state
authStateAtom.batch(() {
authErrorAtom.set(e.toString());
authStateAtom.set(AuthState.error);
});
}
}
Future<void> logout() {
// Implementation details here
}
}
Performance Considerations #
Minimizing Rebuilds #
- Use
AtomSelector
when you only care about a specific part of an atom's state - Use
batch
to group related state updates - Use
throttle
ordebounce
for frequently changing state - Keep atoms small and focused to minimize unnecessary rebuilds
Memory Usage #
- Enable auto-dispose for ephemeral atoms
- Set appropriate dispose timeouts for different kinds of atoms
- Clean up effects and listeners when no longer needed
Comparison with Other Solutions #
Advantages of AtomicFlutter #
- Lightweight: Minimal API surface and small footprint
- Fine-grained reactivity: Only rebuilds what's necessary
- Type safety: Full type safety across the entire state management solution
- Predictable: Direct state updates with clear data flow
- Memory efficient: Automatic disposal of unused state
- Composable: Easy to compose atoms and derived state
When to Consider Alternatives #
- For global immutable state with middleware, consider Redux
- For larger applications with extensive middleware needs, consider Bloc
- For simpler state management needs, consider Provider or Riverpod
Real-World Examples #
User Settings #
// Settings atoms
final darkModeAtom = Atom<bool>(false);
final fontSizeAtom = Atom<double>(16.0);
final notificationsEnabledAtom = Atom<bool>(true);
// Computed atoms
final themeAtom = computed<ThemeData>(
() {
final isDarkMode = darkModeAtom.value;
final fontSize = fontSizeAtom.value;
return isDarkMode
? ThemeData.dark().copyWith(
textTheme: TextTheme(
bodyLarge: TextStyle(fontSize: fontSize),
),
)
: ThemeData.light().copyWith(
textTheme: TextTheme(
bodyLarge: TextStyle(fontSize: fontSize),
),
);
},
tracked: [darkModeAtom, fontSizeAtom],
);
// Settings persistence
void initSettings() {
loadSettings().then((settings) {
darkModeAtom.set(settings.darkMode);
fontSizeAtom.set(settings.fontSize);
notificationsEnabledAtom.set(settings.notificationsEnabled);
});
// Save settings when they change
darkModeAtom.effect((value) => saveSettings('darkMode', value));
fontSizeAtom.effect((value) => saveSettings('fontSize', value));
notificationsEnabledAtom.effect((value) => saveSettings('notificationsEnabled', value));
}
Shopping Cart #
// Cart atoms
final cartItemsAtom = Atom<List<CartItem>>([]);
// Derived atoms
final cartTotalAtom = computed<double>(
() => cartItemsAtom.value.fold(
0,
(total, item) => total + item.price * item.quantity,
),
tracked: [cartItemsAtom],
);
final cartItemCountAtom = computed<int>(
() => cartItemsAtom.value.fold(
0,
(total, item) => total + item.quantity,
),
tracked: [cartItemsAtom],
);
// Cart controller
class CartController {
void addToCart(Product product) {
cartItemsAtom.update((items) {
final existingIndex = items.indexWhere(
(item) => item.product.id == product.id,
);
if (existingIndex >= 0) {
// Update existing item quantity
final updatedItems = List<CartItem>.from(items);
final item = updatedItems[existingIndex];
updatedItems[existingIndex] = item.copyWith(
quantity: item.quantity + 1,
);
return updatedItems;
} else {
// Add new item
return [...items, CartItem(product: product)];
}
});
}
void removeFromCart(String productId) {
// Implementation details here
}
}
See the example folder for a full e-commerce app demo.
Conclusion #
AtomicFlutter provides a lightweight, type-safe, and efficient way to manage state in Flutter applications. By focusing on small, composable atoms of state, it enables a reactive programming model with minimal boilerplate.
Whether you're building a simple counter app or a complex e-commerce application, AtomicFlutter's flexible API and powerful features make it a great choice for state management.
For more detailed examples and advanced usage patterns, check out the example applications included in the package.