Event Module
Overview

Event Module

Decoupled communication between modules using an event-driven architecture.

Overview

EventModule extends Module with the ability to listen for and respond to events. It manages listener lifecycle automatically and provides navigation context to event handlers.

Use events when modules need to react to actions from other modules without creating direct dependencies.

Quick Start

1. Define an Event

class UserLoggedInEvent {
  final String userId;
  UserLoggedInEvent({required this.userId});
}

2. Create an Event Module

class AuthModule extends EventModule {
  @override
  void listen() {
    on<UserLoggedInEvent>((event, context) {
      if (context != null) {
        context.go('/dashboard');
      }
    });
  }
 
  @override
  List<ModularRoute> get routes => [
    ChildRoute('/login', child: (_, __) => LoginPage()),
  ];
}

3. Fire Events

ModularEvent.fire(UserLoggedInEvent(userId: '123'));

Any module listening for UserLoggedInEvent will receive it automatically.

Event Types

Regular Events

Multiple modules can listen to the same event. All listeners are called:

@override
void listen() {
  on<DataUpdatedEvent>((event, context) {
    print('Data: ${event.data}');
  });
}

Exclusive Events

Only one module receives the event at a time (FIFO queue). Use this for events that should be handled exactly once:

@override
void listen() {
  on<ImportantEvent>((event, context) {
    print('Handled: ${event.message}');
  }, exclusive: true);
}

Navigation Context

Event handlers receive a BuildContext? parameter. This context is injected automatically and can be used for navigation:

on<ShowProfileEvent>((event, context) {
  if (context != null) {
    context.go('/profile/${event.userId}');
  }
});

Always check for null before using the context.

Complete Example

// Events
class UserLoggedInEvent {
  final String userId;
  UserLoggedInEvent({required this.userId});
}
 
class ShowSnackBarEvent {
  final String message;
  ShowSnackBarEvent({required this.message});
}
 
// Auth module listens for login
class AuthModule extends EventModule {
  @override
  void listen() {
    on<UserLoggedInEvent>((event, context) {
      if (context != null) {
        context.go('/dashboard');
      }
    });
  }
 
  @override
  List<ModularRoute> get routes => [
    ChildRoute('/login', child: (_, __) => LoginPage()),
  ];
}
 
// UI module listens for snackbar events
class UIModule extends EventModule {
  @override
  void listen() {
    on<ShowSnackBarEvent>((event, context) {
      if (context != null) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text(event.message)),
        );
      }
    });
  }
}
 
// Fire events from anywhere
ModularEvent.fire(UserLoggedInEvent(userId: '123'));
ModularEvent.fire(ShowSnackBarEvent(message: 'Welcome!'));

Lifecycle

EventWhat happens
Module initializeslisten() is called, listeners are registered
Event is firedAll registered listeners for that event type are invoked
Module is disposedListeners are automatically unregistered

No manual cleanup is required.

When to Use

Use caseEventModule?
Cross-module notifications (auth, analytics)Yes
Navigation triggers from business logicYes
UI updates across module boundariesYes
Simple parent-child communicationNo — use callbacks
Direct data passing between widgetsNo — use DI
High-frequency data streamsNo — use streams

Best Practices

  1. Always check context != null before navigating in event handlers.
  2. Use descriptive event namesUserLoggedInEvent, not Event1.
  3. Keep events as plain data — No business logic in event classes.
  4. Use exclusive: true sparingly — Only when the event must be handled exactly once.
  5. Define events close to the producer — The module that fires the event should own the event class.

Listening in Widgets

To listen to events inside a StatefulWidget without creating a full module, use ModularEventMixin. It provides the same on<E>() API and cancels subscriptions automatically when the widget is disposed.

See ModularEventMixin for the full reference.