Event Module
ModularEventMixin

ModularEventMixin

Listen to events directly inside a StatefulWidget — subscriptions are cancelled automatically when the widget is disposed.

Overview

ModularEventMixin is the widget-level counterpart of EventModule. While EventModule handles events inside modules, ModularEventMixin brings the same on<E>() API to any State<T> without requiring a separate module.

EventModuleModularEventMixin
Used inModule subclassesState<T> subclasses
Lifecycle ownerModule (route-scoped)Widget (widget tree–scoped)
Auto-disposeOn module disposeOn widget dispose
Navigation contextGlobal navigator keyWidget's own BuildContext

Setup

Add ModularEventMixin to any State<T>:

counter_page.dart
import 'package:flutter/material.dart';
import 'package:go_router_modular/go_router_modular.dart';
 
class CounterPage extends StatefulWidget {
  const CounterPage({super.key});
 
  @override
  State<CounterPage> createState() => _CounterPageState();
}
 
class _CounterPageState extends State<CounterPage>
    with ModularEventMixin {
  int _count = 0;
 
  @override
  void initState() {
    super.initState();
    on<IncrementEvent>((event, context) {
      setState(() => _count += event.amount);
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(child: Text('$_count')),
    );
  }
}

Call on<E>() in initState (or anywhere before the widget is disposed). No dispose override needed — the mixin handles cleanup automatically.

The on<E>() Method

on<E>(
  void Function(E event, BuildContext? context) callback, {
  EventBus? eventBus,
  bool exclusive = false,
})
ParameterDefaultDescription
callbackrequiredCalled when an event of type E is fired
eventBusglobal busCustom EventBus to listen on (optional)
exclusivefalseWhen true, uses a broadcast stream

Context in Callbacks

The BuildContext? passed to the callback is the widget's own context, available while the widget is mounted:

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

context is null if the widget is unmounted when the event fires. Always guard with a null check before using it.

Replacing a Listener

Calling on<E>() a second time for the same type cancels the first listener and registers the new one:

// Only the second listener will be active
on<SearchEvent>((event, _) => print('old'));
on<SearchEvent>((event, _) => print('new')); // replaces the first

Exclusive Mode

When exclusive: true, the underlying stream becomes a broadcast stream. Use this when the same event type needs to be handled by a single active widget at a time:

on<FullscreenRequestEvent>(
  (event, _) => _enterFullscreen(),
  exclusive: true,
);

Lifecycle

The mixin overrides dispose to cancel all subscriptions automatically:

@override
void dispose() {
  // All subscriptions are cancelled here — no manual cleanup needed
  super.dispose();
}
Widget stateSubscription state
initState calledon<E>() registers listener
Widget is mountedCallbacks are delivered
Widget is disposedAll subscriptions cancelled

Registering Outside initState

on<E>() can be called at any point in the widget lifecycle — in response to user interaction, after an async operation, or in didChangeDependencies:

void _enableNotifications() {
  on<PushNotificationEvent>((event, _) {
    _showBanner(event.message);
  });
}

Listeners registered after initState follow the same lifecycle rules — they are cancelled when the widget is disposed.

Custom EventBus

To listen on a bus other than the global one, pass eventBus:

final _localBus = EventBus();
 
@override
void initState() {
  super.initState();
  on<LocalEvent>(
    (event, _) => _handleLocal(event),
    eventBus: _localBus,
  );
}

Events fired on the global bus will not reach this listener, and vice versa.

Complete Example

dashboard_page.dart
import 'package:flutter/material.dart';
import 'package:go_router_modular/go_router_modular.dart';
 
class UserUpdatedEvent {
  final String name;
  const UserUpdatedEvent(this.name);
}
 
class LogoutEvent {
  const LogoutEvent();
}
 
class DashboardPage extends StatefulWidget {
  const DashboardPage({super.key});
 
  @override
  State<DashboardPage> createState() => _DashboardPageState();
}
 
class _DashboardPageState extends State<DashboardPage>
    with ModularEventMixin {
  String _username = 'Guest';
 
  @override
  void initState() {
    super.initState();
 
    on<UserUpdatedEvent>((event, _) {
      setState(() => _username = event.name);
    });
 
    on<LogoutEvent>((event, context) {
      if (context != null) {
        context.go('/login');
      }
    });
  }
 
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Hello, $_username')),
      body: TextButton(
        onPressed: () => ModularEvent.fire(const LogoutEvent()),
        child: const Text('Logout'),
      ),
    );
  }
}

When to Use

ScenarioUse
Widget reacts to global state changes (theme, auth, settings)ModularEventMixin
Widget drives navigation in response to business eventsModularEventMixin
Multiple widgets share the same event subscriptionModularEventMixin (one per widget)
Business logic reacts to events (not UI)EventModule
Event must be handled exactly once across all modulesEventModule with exclusive: true