EventModule Technical Documentation
A comprehensive event-driven architecture system for Flutter applications using go_router_modular.
Table of Contents
- Overview
- Core Concepts
- Installation & Setup
- Basic Usage
- Advanced Features
- API Reference
- Best Practices
- Performance Considerations
- Migration Guide
- Troubleshooting
Overview
EventModule provides a robust event-driven communication system that enables decoupled architecture between different parts of your Flutter application. It supports both regular and exclusive event handling with automatic memory management.
Key Features
- ✅ Decoupled Communication - Modules communicate without direct dependencies
- ✅ Exclusive Event Handling - FIFO queue system for exclusive listeners
- ✅ Automatic Memory Management - Prevents memory leaks with auto-dispose
- ✅ Context-Aware Events - Automatic NavigatorState context injection
- ✅ Custom EventBus Support - Isolate events between different systems
- ✅ TypeSafe Events - Compile-time type checking for all events
- ✅ Hot Reload Support - Maintains state during development
Core Concepts
Event Types
Events are simple data classes that carry information:
class UserLoginEvent {
final String userId;
final String username;
final DateTime timestamp;
UserLoginEvent({
required this.userId,
required this.username,
required this.timestamp,
});
}
class ApiErrorEvent {
final String endpoint;
final int statusCode;
final String message;
ApiErrorEvent({
required this.endpoint,
required this.statusCode,
required this.message,
});
}
EventModule
Abstract base class for creating event-driven modules:
class AuthModule extends EventModule {
@override
List<ModularRoute> get routes => [
ChildRoute('/login', child: (context, state) => LoginPage()),
ChildRoute('/register', child: (context, state) => RegisterPage()),
];
@override
void listen() {
// Handle user login events
on<UserLoginEvent>((UserLoginEvent event, BuildContext? context) {
print('User ${event.username} logged in at ${event.timestamp}');
if (context != null) {
context.go('/dashboard');
}
});
// Handle user logout events
on<UserLogoutEvent>((UserLogoutEvent event, BuildContext? context) {
print('User ${event.username} logged out');
if (context != null) {
context.go('/login');
}
});
// Handle API errors (exclusive - only one handler at a time)
on<ApiErrorEvent>((ApiErrorEvent event, BuildContext? context) {
print('API Error on ${event.endpoint}: ${event.message}');
if (context != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error: ${event.message}')),
);
}
}, exclusive: true);
}
}
ModularEvent
Global event dispatcher for firing events from anywhere in your application:
// Fire events from anywhere
ModularEvent.fire(UserLoginEvent(
userId: '123',
username: 'john_doe',
timestamp: DateTime.now(),
));
// Listen to events globally (outside modules)
ModularEvent.instance.on<UserLoginEvent>((UserLoginEvent event, BuildContext? context) {
print('User ${event.username} logged in');
});
Installation & Setup
1. Configure GoRouterModular in main.dart
The EventModule system is configured through GoRouterModular.configure()
in your main.dart
:
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Configure GoRouterModular with EventBus settings
await GoRouterModular.configure(
appModule: AppModule(), // Your root module
initialRoute: '/login',
// EventBus Configuration
debugLogEventBus: true, // Enable event logging (🔥🎭 logs)
autoDisposeEventsBus: true, // Auto-dispose listeners when modules are destroyed
// Other GoRouter settings
debugLogDiagnosticsGoRouter: false,
delayDisposeMilliseconds: 1000,
);
runApp(MyApp());
}
2. Create Your App Widget
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'My App',
routerConfig: GoRouterModular.routerConfig,
);
}
}
3. Create Your Root Module
class AppModule extends Module {
@override
List<Module> get imports => [
AuthModule(),
ShoppingModule(),
NotificationModule(),
];
@override
List<ModularRoute> get routes => [
ChildRoute('/', child: (context, state) => HomePage()),
ChildRoute('/login', child: (context, state) => LoginPage()),
];
}
Basic Usage
Creating Events
// Simple event
class ButtonClickedEvent {
final String buttonId;
ButtonClickedEvent(this.buttonId);
}
// Complex event with multiple properties
class ShoppingCartUpdatedEvent {
final String userId;
final List<CartItem> items;
final double totalPrice;
final DateTime timestamp;
ShoppingCartUpdatedEvent({
required this.userId,
required this.items,
required this.totalPrice,
required this.timestamp,
});
}
// Event with optional properties
class NotificationEvent {
final String title;
final String message;
final NotificationType type;
final Duration? duration;
NotificationEvent({
required this.title,
required this.message,
required this.type,
this.duration,
});
}
Creating EventModules
class ShoppingModule extends EventModule {
final CartService _cartService = CartService();
@override
List<ModularRoute> get routes => [
ChildRoute('/shop', child: (context, state) => ShopPage()),
ChildRoute('/cart', child: (context, state) => CartPage()),
ChildRoute('/checkout', child: (context, state) => CheckoutPage()),
];
@override
void listen() {
// Regular listeners (all receive events)
on<AddToCartEvent>((AddToCartEvent event, BuildContext? context) {
_cartService.addItem(event.item);
// Fire another event
ModularEvent.fire(ShoppingCartUpdatedEvent(
userId: event.userId,
items: _cartService.items,
totalPrice: _cartService.totalPrice,
timestamp: DateTime.now(),
));
// Navigate if needed
if (context != null && event.goToCart) {
context.go('/cart');
}
});
on<RemoveFromCartEvent>((RemoveFromCartEvent event, BuildContext? context) {
_cartService.removeItem(event.itemId);
// Fire cart updated event
ModularEvent.fire(ShoppingCartUpdatedEvent(
userId: event.userId,
items: _cartService.items,
totalPrice: _cartService.totalPrice,
timestamp: DateTime.now(),
));
});
// Exclusive listener (only one active at a time)
on<ProcessPaymentEvent>((ProcessPaymentEvent event, BuildContext? context) {
// Only one payment can be processed at a time
// Other payment attempts will queue up
print('Processing payment for ${event.amount}');
_processPayment(event.paymentData);
}, exclusive: true);
// Persistent listener (survives module disposal)
on<AppStartEvent>((AppStartEvent event, BuildContext? context) {
// Initialize cart when app starts
_cartService.initialize();
print('Cart service initialized');
}, autoDispose: false);
}
}
Firing Events
class ShopPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView.builder(
itemBuilder: (context, index) {
return ProductTile(
product: products[index],
onAddToCart: () {
// Fire event when button is pressed
ModularEvent.fire(AddToCartEvent(
userId: currentUserId,
item: products[index],
goToCart: false,
));
},
);
},
),
);
}
}
Advanced Features
Exclusive Event Handling
Exclusive events use a FIFO queue system where only one listener is active at a time:
class PaymentModule extends EventModule {
@override
void listen() {
// Only one payment processor can be active
on<ProcessPaymentEvent>((ProcessPaymentEvent event, BuildContext? context) {
_processPayment(event);
}, exclusive: true);
}
}
class BackupPaymentModule extends EventModule {
@override
void listen() {
// This will queue behind PaymentModule
on<ProcessPaymentEvent>((ProcessPaymentEvent event, BuildContext? context) {
_processPaymentBackup(event);
}, exclusive: true);
}
}
// When PaymentModule is disposed, BackupPaymentModule automatically becomes active
Custom EventBus
Isolate events between different systems:
class AnalyticsModule extends EventModule {
final EventBus analyticsEventBus = EventBus();
AnalyticsModule() : super(eventBus: analyticsEventBus);
@override
void listen() {
// Only listens to events fired on analyticsEventBus
on<AnalyticsEvent>((AnalyticsEvent event, BuildContext? context) {
print('Tracking: ${event.action} at ${event.timestamp}');
_analytics.track(event.action, event.properties);
});
}
}
// Fire events to specific EventBus
ModularEvent.fire(
AnalyticsEvent(action: 'button_click'),
eventBus: analyticsEventBus,
);
Context Handling
The context
parameter provides access to the current NavigatorState:
on<NavigationEvent>((NavigationEvent event, BuildContext? context) {
if (context != null) {
// Safe to navigate
context.go(event.route);
// Access other context features
final theme = Theme.of(context);
final mediaQuery = MediaQuery.of(context);
// Show dialogs
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(event.title),
content: Text(event.message),
),
);
} else {
// Context not available (e.g., during app initialization)
// Handle gracefully or defer the action
_deferredActions.add(() => context?.go(event.route));
}
});
Memory Management
class TemporaryModule extends EventModule {
@override
void listen() {
// Auto-disposed when module is disposed (default)
on<TempEvent>((TempEvent event, BuildContext? context) {
print('Handling temporary event: ${event.data}');
// This listener will be automatically disposed
}, autoDispose: true);
// Survives module disposal (use carefully!)
on<GlobalEvent>((GlobalEvent event, BuildContext? context) {
print('Handling global event: ${event.message}');
// This listener will survive module disposal
}, autoDispose: false);
}
@override
void dispose() {
// Clean up custom resources
_customSubscription?.cancel();
_timer?.cancel();
// Parent handles event subscriptions automatically
super.dispose();
}
}
API Reference
EventModule
Abstract Methods
abstract class EventModule extends Module {
// Implement to define event listeners
void listen();
}
Event Registration
void on<T>(
void Function(T event, BuildContext? context) callback, {
bool? autoDispose, // Auto-dispose when module is destroyed
bool exclusive = false, // Use exclusive queue system
})
// Example usage:
on<MyEvent>((MyEvent event, BuildContext? context) {
// Handle the event...
});
Lifecycle Methods
@override
void initState(Injector i) {
// Called when module is initialized
// listen() is called automatically
}
@override
void dispose() {
// Called when module is destroyed
// Event subscriptions are cleaned automatically
}
ModularEvent
Static Methods
// Fire event globally
static void fire<T>(T event, {EventBus? eventBus})
Instance Methods
// Register global listener
void on<T>(
void Function(T event, BuildContext? context) callback, {
EventBus? eventBus,
bool exclusive = false,
})
// Example usage:
ModularEvent.instance.on<MyEvent>((MyEvent event, BuildContext? context) {
// Handle the event...
});
// Remove specific listener
void dispose<T>({EventBus? eventBus})
GoRouterModular Configuration Parameters
// Available EventBus configuration parameters in GoRouterModular.configure()
await GoRouterModular.configure(
appModule: AppModule(), // Required: Root module
initialRoute: '/home', // Required: Initial route
// EventBus specific parameters
debugLogEventBus: true, // Enable 🔥🎭 event logs (default: false)
autoDisposeEventsBus: true, // Auto-dispose listeners (default: true)
// Other parameters
delayDisposeMilliseconds: 1000, // Delay before disposing modules (default: 1000)
debugLogDiagnosticsGoRouter: false, // GoRouter debug logs (default: false)
// ... other GoRouter parameters
);
Best Practices
1. Event Design
// ✅ GOOD: Immutable events with clear purpose
class UserProfileUpdatedEvent {
final String userId;
final UserProfile profile;
final DateTime updatedAt;
const UserProfileUpdatedEvent({
required this.userId,
required this.profile,
required this.updatedAt,
});
}
// ❌ BAD: Mutable events with unclear purpose
class GenericUpdateEvent {
String? data;
Map<String, dynamic>? payload;
}
2. Context Handling
// ✅ GOOD: Always check context
on<NavigationEvent>((NavigationEvent event, BuildContext? context) {
if (context != null) {
context.go(event.route);
} else {
// Handle gracefully
_logger.warning('Navigation attempted without context');
}
});
// ❌ BAD: Assuming context is available
on<NavigationEvent>((NavigationEvent event, BuildContext? context) {
context!.go(event.route); // May throw!
});
3. Error Handling
// ✅ GOOD: Proper error handling
on<ApiCallEvent>((ApiCallEvent event, BuildContext? context) async {
try {
final result = await apiService.call(event.endpoint);
ModularEvent.fire(ApiSuccessEvent(result));
} catch (error) {
ModularEvent.fire(ApiErrorEvent(
endpoint: event.endpoint,
error: error.toString(),
));
}
});
4. Module Organization
// ✅ GOOD: Focused modules with single responsibility
class AuthModule extends EventModule {
// Only handles authentication-related events
}
class ShoppingModule extends EventModule {
// Only handles shopping-related events
}
// ❌ BAD: God module handling everything
class AppModule extends EventModule {
// Handles auth, shopping, notifications, analytics, etc.
}
5. Event Naming
// ✅ GOOD: Descriptive event names
class UserLoginSucceededEvent { }
class ShoppingCartItemAddedEvent { }
class PaymentProcessingStartedEvent { }
// ❌ BAD: Generic event names
class UserEvent { }
class DataEvent { }
class ActionEvent { }
Performance Considerations
1. Event Frequency
// ✅ GOOD: Debounced high-frequency events
class SearchModule extends EventModule {
Timer? _debounceTimer;
@override
void listen() {
on<SearchQueryChangedEvent>((SearchQueryChangedEvent event, BuildContext? context) {
// Debounce search to avoid too many API calls
_debounceTimer?.cancel();
_debounceTimer = Timer(Duration(milliseconds: 300), () {
_performSearch(event.query);
});
});
}
}
// ❌ BAD: High-frequency events without debouncing
on<MouseMoveEvent>((MouseMoveEvent event, BuildContext? context) {
_performExpensiveOperation(); // Called on every mouse move!
});
2. Memory Management
// ✅ GOOD: Clean up custom resources
@override
void dispose() {
_customController.dispose();
_animationController.dispose();
_streamSubscription.cancel();
super.dispose(); // Always call super.dispose()
}
// ❌ BAD: Forgetting to clean up
@override
void dispose() {
super.dispose();
// Forgot to clean up custom resources!
}
3. Event Payload Size
// ✅ GOOD: Lightweight events with IDs
class UserUpdatedEvent {
final String userId;
UserUpdatedEvent(this.userId);
}
// In your module:
@override
void listen() {
on<UserUpdatedEvent>((UserUpdatedEvent event, BuildContext? context) {
final user = userService.getUser(event.userId);
_updateUI(user);
});
}
// ❌ BAD: Heavy events with full data
class UserUpdatedEvent {
final User user; // Could be megabytes of data
final List<Permission> permissions;
final List<ActivityLog> activityLogs;
}
Migration Guide
From v3.x to v4.x
Breaking Changes
-
broadcast
parameter deprecated:// OLD (deprecated) on<MyEvent>((event, context) { }, broadcast: true); // NEW on<MyEvent>((event, context) { }, exclusive: true);
-
Automatic context injection:
// OLD: Manual context passing on<MyEvent>((event) { final context = MyApp.navigatorKey.currentContext; // Use context... }); // NEW: Automatic context injection on<MyEvent>((MyEvent event, BuildContext? context) { if (context != null) { // Use context directly context.go('/new-route'); } });
Migration Steps
- Replace
broadcast: true
withexclusive: true
- Update event handlers to accept
BuildContext? context
parameter - Add null checks for context usage
- Test exclusive event behavior (FIFO queue vs. last-wins)
Troubleshooting
Common Issues
1. Events Not Received
Problem: Event listeners not receiving events
Solutions:
// Check if module is properly initialized
class MyModule extends EventModule {
@override
void initState(Injector i) {
super.initState(i); // Don't forget this!
}
}
// Verify event types match exactly
ModularEvent.fire(UserLoginEvent()); // Event type: UserLoginEvent
on<UserLoginEvent>((UserLoginEvent event, BuildContext? context) { }); // Listener type: UserLoginEvent
2. Memory Leaks
Problem: Memory not being released
Solutions:
// Ensure proper disposal
@override
void dispose() {
_customSubscription?.cancel();
super.dispose(); // Always call super.dispose()
}
// Check autoDispose settings
on<MyEvent>((MyEvent event, BuildContext? context) { }, autoDispose: true); // Default
3. Context Not Available
Problem: context
is null in event handlers
Solutions:
on<NavigationEvent>((NavigationEvent event, BuildContext? context) {
if (context != null) {
// Safe to use context
context.go('/route');
} else {
// Defer action or use alternative method
WidgetsBinding.instance.addPostFrameCallback((_) {
final context = MyApp.navigatorKey.currentContext;
if (context != null) {
context.go('/route');
}
});
}
});
4. Exclusive Events Not Working
Problem: Multiple exclusive listeners receiving events
Solutions:
// Ensure all listeners use the same EventBus
class Module1 extends EventModule {
@override
void listen() {
on<MyEvent>((MyEvent event, BuildContext? context) { }, exclusive: true);
}
}
class Module2 extends EventModule {
@override
void listen() {
on<MyEvent>((MyEvent event, BuildContext? context) { }, exclusive: true);
}
}
// Check that clearEventModuleState() is called between tests
setUp(() {
clearEventModuleState(); // In tests only
});
Debug Tools
Enable Debug Logging
// Configure in main.dart
await GoRouterModular.configure(
appModule: AppModule(),
initialRoute: '/home',
debugLogEventBus: true, // Enable event logging
);
// Output:
// 🔥 Event fired: UserLoginEvent
// 🎭 Event received: UserLoginEvent
Clear State (Testing Only)
import 'package:go_router_modular/src/utils/event_module.dart';
setUp(() {
clearEventModuleState(); // Clears all global state
});
Support
For issues, feature requests, or questions:
- 📖 Documentation (opens in a new tab)
- 🐛 Issue Tracker (opens in a new tab)
- 💬 Discussions (opens in a new tab)
Built with ❤️ for the Flutter community