leulit_flutter_actionmanager 5.0.1
leulit_flutter_actionmanager: ^5.0.1 copied to clipboard
A lightweight, type-safe action dispatcher for Flutter applications. Works seamlessly across all layers - UI, domain, data, and services.
example/lib/main.dart
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:leulit_flutter_actionmanager/leulit_flutter_actionmanager.dart';
void main() {
// Configurar logger (opcional)
ActionManager.configureLogger(
enabled: true,
showTimestamp: true,
);
runApp(const MyApp());
}
/// Enum de acciones de ejemplo
enum AppAction {
increment,
decrement,
reset,
randomNumber,
loadData, // Acción para demostrar loading
asyncDataUpdate, // Nueva acción para demostrar dispatchAsync
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Action Dispatcher Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: const HomePage(),
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
int _counter = 0;
bool _isLoading = false;
final _subscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
// ✅ Pattern correcto: usar listen() con cleanup
_subscriptions.add(
ActionManager.listen(AppAction.increment, (event) {
if (mounted) setState(() => _counter++);
}),
);
_subscriptions.add(
ActionManager.listen(AppAction.decrement, (event) {
if (mounted) setState(() => _counter--);
}),
);
_subscriptions.add(
ActionManager.listen(AppAction.reset, (event) {
if (mounted) setState(() => _counter = 0);
}),
);
_subscriptions.add(
ActionManager.listen<int>(AppAction.randomNumber, (event) {
final number = event.data;
if (mounted && number != null) {
setState(() => _counter = number);
}
}),
);
// Handler para simular carga de datos
_subscriptions.add(
ActionManager.listen(AppAction.loadData, (event) {
// Simular operación costosa
Future.delayed(const Duration(seconds: 2), () {
if (mounted) setState(() => _counter = 42);
});
}),
);
// Múltiples handlers para asyncDataUpdate
// Handler 1: Actualización UI rápida
_subscriptions.add(
ActionManager.listen<int>(AppAction.asyncDataUpdate, (event) {
final value = event.data;
if (mounted && value != null) {
setState(() => _counter = value);
}
}),
);
// Handler 2: Operación async (simula guardado en DB)
_subscriptions.add(
ActionManager.listen<int>(AppAction.asyncDataUpdate, (event) async {
final value = event.data;
await Future.delayed(const Duration(milliseconds: 500));
debugPrint('✅ Datos guardados en DB: $value');
}),
);
// Handler 3: Otra operación async (simula envío de analytics)
_subscriptions.add(
ActionManager.listen<int>(AppAction.asyncDataUpdate, (event) async {
final value = event.data;
await Future.delayed(const Duration(milliseconds: 300));
debugPrint('✅ Analytics enviado: $value');
}),
);
}
@override
void dispose() {
// ✅ CRÍTICO: Cancelar todas las subscriptions para prevenir memory leaks
for (final subscription in _subscriptions) {
subscription.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Action Dispatcher Demo'),
actions: [
IconButton(
icon: const Icon(Icons.info_outline),
onPressed: () {
// Mostrar estadísticas
ActionManager.printSummary();
// También en UI
showDialog(
context: context,
builder: (context) => const StatsDialog(),
);
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Counter Value:', style: TextStyle(fontSize: 20)),
Text(
'$_counter',
style: Theme.of(context).textTheme.displayLarge,
),
const SizedBox(height: 40),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.remove),
label: const Text('Decrement'),
onPressed: () {
ActionManager.dispatch(AppAction.decrement);
},
),
const SizedBox(width: 10),
ElevatedButton.icon(
icon: const Icon(Icons.add),
label: const Text('Increment'),
onPressed: () {
ActionManager.dispatch(AppAction.increment);
},
),
],
),
const SizedBox(height: 20),
ElevatedButton.icon(
icon: const Icon(Icons.refresh),
label: const Text('Reset'),
onPressed: () {
ActionManager.dispatch(AppAction.reset);
},
),
const SizedBox(height: 20),
ElevatedButton.icon(
icon: const Icon(Icons.shuffle),
label: const Text('Random Number'),
onPressed: () {
final random = DateTime.now().millisecond;
ActionManager.dispatch(AppAction.randomNumber, data: random);
},
),
const SizedBox(height: 20),
// 🚀 Ejemplo de dispatchAsync con onComplete
ElevatedButton.icon(
icon: _isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Icon(Icons.cloud_upload),
label: Text(_isLoading ? 'Procesando...' : 'Async Update'),
onPressed: _isLoading
? null
: () async {
setState(() => _isLoading = true);
final newValue = DateTime.now().second;
final messenger = ScaffoldMessenger.of(context);
// Usar dispatchAsync para esperar a que TODOS los listeners terminen
await ActionManager.dispatchAsync(
AppAction.asyncDataUpdate,
data: newValue,
onComplete: () {
debugPrint(
'✅ TODOS los listeners completaron (UI, DB, Analytics)');
},
);
// Esta línea se ejecuta DESPUÉS de que todos los listeners terminen
if (mounted) {
setState(() => _isLoading = false);
messenger.showSnackBar(
const SnackBar(
content: Text(
'✅ Actualización completa (UI + DB + Analytics)'),
duration: Duration(seconds: 2),
),
);
}
},
),
const SizedBox(height: 40),
const SecondWidget(),
],
),
),
);
}
}
/// Widget adicional que también escucha las mismas acciones
class SecondWidget extends StatefulWidget {
const SecondWidget({super.key});
@override
State<SecondWidget> createState() => _SecondWidgetState();
}
class _SecondWidgetState extends State<SecondWidget> {
int _localCounter = 0;
final _subscriptions = <StreamSubscription>[];
@override
void initState() {
super.initState();
// Este widget también responde a las acciones
_subscriptions.add(
ActionManager.listen(AppAction.increment, (_) {
if (mounted) setState(() => _localCounter++);
}),
);
_subscriptions.add(
ActionManager.listen(AppAction.reset, (_) {
if (mounted) setState(() => _localCounter = 0);
}),
);
}
@override
void dispose() {
for (final subscription in _subscriptions) {
subscription.cancel();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Text(
'This widget also listens to actions!',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Text('Local counter: $_localCounter'),
],
),
),
);
}
}
/// Dialog que muestra estadísticas
class StatsDialog extends StatelessWidget {
const StatsDialog({super.key});
@override
Widget build(BuildContext context) {
final stats = ActionManager.getStats();
return AlertDialog(
title: const Text('📊 Dispatcher Stats'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Total Actions: ${stats.totalActions}'),
Text('Total Handlers: ${stats.totalHandlers}'),
Text('Total Dispatches: ${stats.totalDispatches}'),
const SizedBox(height: 16),
const Text('Action Details:',
style: TextStyle(fontWeight: FontWeight.bold)),
...AppAction.values.map((action) {
final metadata = ActionManager.getMetadata(action);
return Padding(
padding: const EdgeInsets.only(top: 8),
child: Text(
'${action.name}: ${metadata.handlerCount} handlers, '
'${metadata.dispatchCount} dispatches',
style: const TextStyle(fontSize: 12),
),
);
}),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
);
}
}