leulit_flutter_actionmanager 5.0.0 copy "leulit_flutter_actionmanager: ^5.0.0" to clipboard
leulit_flutter_actionmanager: ^5.0.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.

pub package License: MIT

✨ 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: ^5.0.0

Then run:

flutter pub get

📚 Basic Usage #

1. Define your actions #

enum AppAction {
  userLoggedIn,
  userLoggedOut,
  dataLoaded,
  networkError,
  zoomIn,
  zoomOut,
}

2. Dispatch actions #

⚠️ Important: All handlers execute asynchronously (microtask queue) after dispatch returns. This means handlers run after the current synchronous code completes.

// 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 #

✅ Best Practice: Use in StatefulWidget with cleanup

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final _subscriptions = <StreamSubscription>[];

  @override
  void initState() {
    super.initState();
    
    // ✅ RECOMMENDED: Use listen() with cleanup
    _subscriptions.add(
      ActionManager.listen<User>(AppAction.userLoggedIn, (event) {
        if (mounted) { // Always check mounted before setState
          final user = event.data;
          setState(() {
            // Update state with user
          });
        }
      }),
    );
  }

  @override
  void dispose() {
    // ✅ CRITICAL: Cancel all subscriptions
    for (final sub in _subscriptions) {
      sub.cancel();
    }
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Container();
  }
}

⚠️ Alternative: Use on() with manual cleanup

class _MyWidgetState extends State<MyWidget> {
  String? _handlerId;

  @override
  void initState() {
    super.initState();
    
    // ⚠️ If using on(), you MUST cleanup manually
    _handlerId = ActionManager.on<User>(AppAction.userLoggedIn, (event) {
      if (mounted) setState(() { /* Update with event.data */ });
    });
  }

  @override
  void dispose() {
    // ✅ CRITICAL: Remove handler
    if (_handlerId != null) {
      ActionManager.off(AppAction.userLoggedIn, _handlerId!);
    }
    super.dispose();
  }
}

🎯 Best: Use ActionManagerWidget (auto cleanup)

// ✅ BEST PRACTICE: Automatic cleanup, no manual subscription management
ActionManagerWidget<User>(
  action: AppAction.userLoggedIn,
  builder: (context, event) {
    final user = event.data;
    return Text('Welcome ${user?.name ?? "Guest"}');
  },
)

Other handler types

// Simple handler (no data)
ActionManager.onVoid(AppAction.zoomIn, (event) {
  print('Zooming in at ${event.timestamp}!');
});

// Handler with data and ActionEvent metadata
ActionManager.on<String>(AppAction.networkError, (event) {
  print('Error at ${event.timestamp}: ${event.data}');
  print('Context: ${event.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, event) {
    return Text('Status: ${event.data ?? "idle"}');
  },
)

// With placeholder before first event
ActionManagerWidget<User>(
  action: AppAction.userLoggedIn,
  placeholder: CircularProgressIndicator(),
  builder: (context, event) {
    final user = event.data;
    return Text('Welcome ${user?.name ?? "Guest"}!');
  },
)

// Debugging: access full event metadata
ActionManagerWidget<bool>(
  action: AppAction.displayForm,
  builder: (context, event) {
    debugPrint('Action: ${event.action}, Display: ${event.data}');
    debugPrint('Source: ${event.context?["source"]}');
    if (event.data == true) return MyForm();
    return const SizedBox.shrink();
  },
)

// 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, (event) {
      final id = event.data;
      if (id != null) _deleteIssue(id);
    });
    
    ActionManager.on<List<Issue>>(AppAction.issuesLoaded, (event) {
      this.issues = event.data ?? [];
      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, (event) async {
      final id = event.data;
      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, (event) {
      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, (event) => ...);

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.

📬 Support #

0
likes
0
points
702
downloads

Publisher

unverified uploader

Weekly Downloads

A lightweight, type-safe action dispatcher for Flutter applications. Works seamlessly across all layers - UI, domain, data, and services.

Repository (GitHub)
View/report issues

Topics

#state-management #architecture #events #actions #dispatcher

License

unknown (license)

Dependencies

flutter, meta

More

Packages that depend on leulit_flutter_actionmanager