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);
// ✅ 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
'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!