leulit_flutter_actionmanager 4.1.0
leulit_flutter_actionmanager: ^4.1.0 copied to clipboard
A lightweight, type-safe action dispatcher for Flutter applications. Works seamlessly across all layers - UI, domain, data, and services.
Leulit Flutter Action Manager #
A lightweight, type-safe action dispatcher for Flutter applications. Works seamlessly across all layers: UI, domain, data, and services.
✨ Features #
- ✅ Type-safe with Dart enums (no string typos!)
- ✅ Zero external dependencies (only Flutter/Dart)
- ✅ Universal: Works in widgets, controllers, services, repositories
- ✅ Reactive: StreamSubscription support for easy cleanup
- ✅ Debuggable: Built-in introspection and statistics
- ✅ Memory-safe: Proper handler cleanup
- ✅ Lightweight: < 300 lines of code
🚀 Installation #
Add to your pubspec.yaml:
dependencies:
leulit_flutter_actionmanager: ^4.1.0
Then run:
flutter pub get
📚 Basic Usage #
1. Define your actions #
enum AppAction {
userLoggedIn,
userLoggedOut,
dataLoaded,
networkError,
zoomIn,
zoomOut,
}
2. Dispatch actions #
// Without data
ActionManager.dispatch(AppAction.zoomIn);
// With data
ActionManager.dispatch(AppAction.userLoggedIn, data: user);
// With additional context
ActionManager.dispatch(
AppAction.networkError,
data: errorMessage,
context: {'retry': true, 'endpoint': '/api/users'},
);
3. Register handlers #
// Simple handler
ActionManager.onVoid(AppAction.zoomIn, () {
print('Zooming in!');
});
// Handler with typed data
ActionManager.on<User>(AppAction.userLoggedIn, (user) {
print('Welcome ${user?.name}!');
});
// Handler with full ActionEvent metadata
ActionManager.onFull<String>(AppAction.networkError, (action) {
print('Error at ${action.timestamp}: ${action.data}');
print('Context: ${action.context}');
});
4. Reactive Widgets (NEW in 4.1.0) #
The easiest way to build reactive UIs - widgets that automatically rebuild when actions are dispatched:
// Simple reactive widget
ActionManagerWidget<String>(
action: AppAction.statusUpdated,
builder: (context, status) {
return Text('Status: ${status ?? "idle"}');
},
)
// With placeholder before first event
ActionManagerWidget<User>(
action: AppAction.userLoggedIn,
placeholder: CircularProgressIndicator(),
builder: (context, user) {
return Text('Welcome ${user?.name ?? "Guest"}!');
},
)
// React to multiple actions
ActionManagerMultiWidget(
actions: [AppAction.refresh, AppAction.dataUpdated],
builder: (context, event) {
return Text('Last update: ${event?.timestamp}');
},
)
Benefits:
- ✅ Auto-subscription and cleanup (no dispose needed)
- ✅ Automatic
setState()when action is dispatched - ✅ Type-safe with generics
- ✅ Optional placeholder widget
🎯 Use Cases #
In Widgets (Manual Subscription) #
class MyWidget extends StatefulWidget {
@override
State<MyWidget> createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
StreamSubscription? _sub;
@override
void initState() {
super.initState();
// Listen and rebuild
_sub = ActionManager.listen(AppAction.dataLoaded, (_) {
setState(() {});
});
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => ActionManager.dispatch(AppAction.zoomIn),
child: Text('Zoom +'),
);
}
}
In Controllers (GetX example) #
class IssueController extends GetxController {
@override
void onInit() {
super.onInit();
ActionManager.on<String>(AppAction.deleteIssue, (id) {
if (id != null) _deleteIssue(id);
});
ActionManager.on<List<Issue>>(AppAction.issuesLoaded, (issues) {
this.issues = issues ?? [];
update(); // Trigger UI rebuild
});
}
void deleteIssue(String id) {
ActionManager.dispatch(AppAction.deleteIssue, data: id);
}
}
In Services/Repositories #
class IssueRepository {
IssueRepository() {
// Listen to UI commands
ActionManager.on<String>(AppAction.deleteIssue, (id) async {
if (id != null) {
await _api.deleteIssue(id);
await loadIssues(); // Reload data
}
});
}
Future<void> loadIssues() async {
try {
final issues = await _api.getIssues();
// Notify entire app
ActionManager.dispatch(AppAction.issuesLoaded, data: issues);
} catch (e) {
ActionManager.dispatch(AppAction.networkError, data: e.toString());
}
}
}
🔍 Debugging & Introspection #
// Check if action has handlers
if (ActionManager.hasHandlers(AppAction.test)) {
print('Someone is listening!');
}
// Get handler count
final count = ActionManager.handlerCount(AppAction.test);
print('Handlers: $count');
// Get detailed metadata
final metadata = ActionManager.getMetadata(AppAction.test);
print(metadata); // Shows count, dispatches, last time, etc.
// Get global statistics
final stats = ActionManager.getStats();
print(stats); // Shows all actions, handlers, dispatches
// Print visual summary
ActionManager.printSummary();
Output example:
📊 ═══════════════════════════════════════════
📊 Action Dispatcher Summary
📊 ═══════════════════════════════════════════
🎯 userLoggedIn
├─ Handlers: 3
├─ Dispatched: 15 times
└─ Last: 2026-02-09 10:30:45.123
🎯 dataLoaded
├─ Handlers: 2
├─ Dispatched: 42 times
└─ Last: 2026-02-09 10:32:10.456
📊 ═══════════════════════════════════════════
📊 Total: 2 actions, 5 handlers, 57 dispatches
📊 ═══════════════════════════════════════════
⚙️ Configuration #
// Configure logger
ActionManager.configureLogger(
enabled: true, // Enable/disable logging
showTimestamp: true, // Include timestamp in logs
);
// Clear all handlers (useful for tests)
ActionManager.clear();
🧪 Testing #
void main() {
setUp(() {
ActionManager.clear(); // Clean slate for each test
});
test('should handle action', () {
bool called = false;
ActionManager.onVoid(AppAction.test, () {
called = true;
});
ActionManager.dispatch(AppAction.test);
expect(called, isTrue);
});
}
🤔 Why ActionManager? #
Before (Multiple paradigms) #
// UI: GetX reactivity
Obx(() => Text(controller.data.value))
// Cross-controller: EventBus
eventBus.fire(MyEvent(data));
// Service → UI: Callbacks
service.onData = (data) => setState(() {});
// Repository: Streams
repo.dataStream.listen((data) => ...);
After (One system) #
// Everywhere:
ActionManager.dispatch(AppAction.dataLoaded, data: data);
ActionManager.on<Data>(AppAction.dataLoaded, (data) => ...);
Benefits:
- One pattern to learn
- Consistent throughout the codebase
- Easy to debug (see all actions in one place)
- No confusion about which system to use
📄 License #
MIT License - see LICENSE file for details.
🤝 Contributing #
Contributions are welcome! Please feel free to submit a Pull Request.