The Events System
The events system in Mosaic is like a message board where modules can post messages and read messages from others. It’s how modules communicate without needing to know about each other directly.
Why Use Events?
Imagine you have a music app with different modules:
- Player Module controls music playback
- Playlist Module manages song lists
- Library Module shows all your music
When a song ends, the Player Module needs to tell the Playlist Module to play the next song. But what if the Player Module doesn’t know about the Playlist Module?
That’s where events come in. The Player Module can shout “Song ended!” and any module that cares (like the Playlist Module) can hear it and respond.
Basic Events
Sending a Message (Emitting)
To send a message, use events.emit()
:
// Send a simple text message
events.emit<String>('user/logged_in', 'john_doe');
// Send a boolean (true/false)
events.emit<bool>('settings/dark_mode', true);
// Send numbers
events.emit<int>('player/volume', 75);
// Send complex data
events.emit<Map<String, dynamic>>('order/completed', {
'orderId': '12345',
'total': 99.99,
'items': ['coffee', 'sandwich']
});
Listening for Messages
To listen for messages, use events.on()
:
// Listen for user login
events.on<String>('user/logged_in', (context) {
String username = context.data;
print('Welcome back, $username!');
});
// Listen for settings changes
events.on<bool>('settings/dark_mode', (context) {
bool isDarkMode = context.data;
updateTheme(isDarkMode);
});
// Listen for order completion
events.on<Map<String, dynamic>>('order/completed', (context) {
Map<String, dynamic> orderData = context.data;
showThankYouMessage(orderData['orderId']);
});
Event Names
Event names are like addresses. They help organize messages by topic:
// User-related events
'user/logged_in'
'user/logged_out'
'user/profile_updated'
// Settings events
'settings/theme_changed'
'settings/language_changed'
'settings/notifications_enabled'
// Shopping cart events
'cart/item_added'
'cart/item_removed'
'cart/checkout_started'
Think of the /
as folders on your computer - it helps group related events together.
Wildcard Listening
Sometimes you want to listen to multiple related events at once:
Single Wildcard (*)
Listen to immediate children only:
// Listen to all user events at the top level
events.on<dynamic>('user/*', (context) {
print('User event: ${context.name}');
});
// This catches:
// user/logged_in ✅
// user/logged_out ✅
// user/profile/updated ❌ (too deep)
Multi Wildcard (#)
Listen to all events under a path, no matter how deep:
// Listen to ALL user-related events
events.on<dynamic>('user/#', (context) {
print('Any user event: ${context.name}');
});
// This catches:
// user/logged_in ✅
// user/logged_out ✅
// user/profile/updated ✅
// user/settings/theme/changed ✅
Real-World Example
Let’s build a simple notification system:
Notification Module
class NotificationModule extends Module {
NotificationModule() : super(name: 'notifications');
List<String> notifications = [];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Notifications')),
body: ListView(
children: notifications.map((notification) =>
ListTile(title: Text(notification))
).toList(),
),
);
}
@override
Future<void> onInit() async {
// Listen for all kinds of events we care about
// User events
events.on<String>('user/logged_in', (context) {
addNotification('Welcome back, ${context.data}!');
});
// Order events
events.on<Map<String, dynamic>>('order/completed', (context) {
String orderId = context.data['orderId'];
addNotification('Your order $orderId is complete!');
});
// Message events
events.on<String>('message/received', (context) {
addNotification('New message: ${context.data}');
});
// Any error from anywhere in the app
events.on<String>('*/error', (context) {
addNotification('Something went wrong: ${context.data}');
});
}
void addNotification(String message) {
notifications.insert(0, message); // Add to top of list
// Keep only the last 20 notifications
if (notifications.length > 20) {
notifications.removeLast();
}
logger.info('New notification: $message');
}
}
Other Modules Sending Events
class UserModule extends Module {
Future<void> login(String username, String password) async {
try {
// Do login logic
await authService.login(username, password);
// Tell everyone the user logged in
events.emit<String>('user/logged_in', username);
} catch (error) {
// Tell everyone there was an error
events.emit<String>('user/error', 'Login failed: ${error.toString()}');
}
}
}
class ShoppingModule extends Module {
Future<void> completeOrder(Order order) async {
try {
// Process the order
await orderService.process(order);
// Tell everyone the order is done
events.emit<Map<String, dynamic>>('order/completed', {
'orderId': order.id,
'total': order.total,
'customerName': order.customerName,
});
} catch (error) {
events.emit<String>('order/error', 'Order failed: ${error.toString()}');
}
}
}
Now the NotificationModule automatically gets notifications whenever users log in, orders complete, or errors happen - without any of the other modules needing to know about it!
Event Context
When you listen to an event, you get a context
object with useful information:
events.on<String>('user/message', (context) {
// The data that was sent
String messageText = context.data;
// The full event name
String eventName = context.name; // 'user/message'
// You can also access event path parts
print('Event: $eventName, Data: $messageText');
});
Advanced Event Patterns
Request-Response Pattern
Sometimes you want to ask a question and get an answer:
// Module A asks for user data
events.emit<String>('data/request/user', 'user123');
// Module B responds with the data
events.on<String>('data/request/user', (context) {
String userId = context.data;
User user = getUserFromDatabase(userId);
// Send the response back
events.emit<User>('data/response/user/${userId}', user);
});
// Module A listens for the response
events.on<User>('data/response/user/user123', (context) {
User userData = context.data;
displayUser(userData);
});
Broadcast Pattern
Tell everyone about something important:
// When the app goes offline
events.emit<bool>('app/network_status', false);
// Multiple modules can react
class UIModule extends Module {
@override
Future<void> onInit() async {
events.on<bool>('app/network_status', (context) {
bool isOnline = context.data;
showOfflineBanner(!isOnline);
});
}
}
class DataModule extends Module {
@override
Future<void> onInit() async {
events.on<bool>('app/network_status', (context) {
bool isOnline = context.data;
if (isOnline) {
syncPendingData();
} else {
pauseDataSync();
}
});
}
}
Chain Events
Use events to create workflows:
// Step 1: User starts checkout
events.emit<Map<String, dynamic>>('checkout/started', {
'cartId': 'cart123',
'userId': 'user456'
});
// Step 2: Payment module processes payment
events.on<Map<String, dynamic>>('checkout/started', (context) async {
Map<String, dynamic> checkoutData = context.data;
try {
await processPayment(checkoutData);
events.emit<Map<String, dynamic>>('payment/completed', checkoutData);
} catch (error) {
events.emit<Map<String, dynamic>>('payment/failed', {
...checkoutData,
'error': error.toString()
});
}
});
// Step 3: Inventory module updates stock
events.on<Map<String, dynamic>>('payment/completed', (context) {
updateInventory(context.data['cartId']);
events.emit<Map<String, dynamic>>('inventory/updated', context.data);
});
// Step 4: Email module sends confirmation
events.on<Map<String, dynamic>>('inventory/updated', (context) {
sendConfirmationEmail(context.data['userId']);
events.emit<Map<String, dynamic>>('checkout/completed', context.data);
});
Event Best Practices
1. Use Clear, Descriptive Names
// ✅ Good - clear what happened
events.emit<String>('user/login_successful', username);
events.emit<int>('cart/item_count_changed', newCount);
// ❌ Bad - unclear what these mean
events.emit<String>('user/event1', username);
events.emit<int>('thing/happened', newCount);
2. Group Related Events
// ✅ Good - organized by feature
'auth/login_started'
'auth/login_completed'
'auth/login_failed'
'auth/logout'
'cart/item_added'
'cart/item_removed'
'cart/checkout_started'
// ❌ Bad - no organization
'login_started'
'item_added'
'logout'
'checkout_started'
3. Include Useful Data
// ✅ Good - includes context
events.emit<Map<String, dynamic>>('order/created', {
'orderId': 'order123',
'userId': 'user456',
'total': 99.99,
'timestamp': DateTime.now().toIso8601String()
});
// ❌ Bad - not enough information
events.emit<String>('order/created', 'order123');
4. Handle Events in onInit()
class MyModule extends Module {
@override
Future<void> onInit() async {
// ✅ Good - set up listeners during initialization
events.on<String>('user/logged_in', handleUserLogin);
events.on<bool>('app/theme_changed', handleThemeChange);
}
// ❌ Bad - don't set up listeners in build() method
@override
Widget build(BuildContext context) {
events.on<String>('user/logged_in', handleUserLogin); // This will create duplicate listeners!
return Scaffold(/* ... */);
}
}
5. Clean Up When Done
class MyModule extends Module {
late StreamSubscription _userSubscription;
late StreamSubscription _themeSubscription;
@override
Future<void> onInit() async {
_userSubscription = events.on<String>('user/logged_in', handleUserLogin);
_themeSubscription = events.on<bool>('app/theme_changed', handleThemeChange);
}
@override
void onDestroy() {
// Clean up listeners
_userSubscription.cancel();
_themeSubscription.cancel();
super.onDestroy();
}
}
Debugging Events
Sometimes events don’t work as expected. Here’s how to debug:
1. Add Logging
class DebugModule extends Module {
@override
Future<void> onInit() async {
// Listen to ALL events for debugging
events.on<dynamic>('*/#', (context) {
logger.info('Event: ${context.name}, Data: ${context.data}');
});
}
}
2. Check Event Names
// Make sure names match exactly (case-sensitive)
events.emit<String>('user/logged_in', username); // Sender
events.on<String>('user/logged_in', handleLogin); // Receiver - matches ✅
events.emit<String>('user/logged_in', username); // Sender
events.on<String>('user/loggedIn', handleLogin); // Receiver - doesn't match ❌
3. Check Data Types
// Make sure types match
events.emit<String>('user/id', 'user123'); // Sending String
events.on<String>('user/id', (context) { ... }); // Expecting String ✅
events.emit<int>('user/id', 123); // Sending int
events.on<String>('user/id', (context) { ... }); // Expecting String ❌
Testing Events
Test your event handling:
void main() {
group('Event Tests', () {
test('should handle user login event', () async {
String? receivedUsername;
// Set up listener
events.on<String>('user/logged_in', (context) {
receivedUsername = context.data;
});
// Send event
events.emit<String>('user/logged_in', 'john_doe');
// Give events time to process
await Future.delayed(Duration.zero);
// Check result
expect(receivedUsername, equals('john_doe'));
});
});
}
Common Event Patterns in Apps
Authentication Flow
'auth/login_started' // User clicked login button
'auth/login_loading' // Showing loading spinner
'auth/login_successful' // Login worked
'auth/login_failed' // Login didn't work
'auth/logout' // User logged out
Shopping Cart
'cart/item_added' // Item added to cart
'cart/item_removed' // Item removed from cart
'cart/quantity_changed' // Changed item quantity
'cart/cleared' // Cart emptied
'cart/checkout_started' // User started checkout
Navigation
'nav/module_changed' // Switched to different module
'nav/page_pushed' // Opened new page within module
'nav/page_popped' // Closed page within module
'nav/back_pressed' // User pressed back button
App State
'app/started' // App finished loading
'app/network_online' // Internet connection restored
'app/network_offline' // Internet connection lost
'app/background' // App moved to background
'app/foreground' // App brought back to foreground
The events system is what makes modules truly independent while still allowing them to work together. Start with simple events and gradually build more complex communication patterns as your app grows!