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.
EventModule | ModularEventMixin | |
|---|---|---|
| Used in | Module subclasses | State<T> subclasses |
| Lifecycle owner | Module (route-scoped) | Widget (widget tree–scoped) |
| Auto-dispose | On module dispose | On widget dispose |
| Navigation context | Global navigator key | Widget's own BuildContext |
Setup
Add ModularEventMixin to any State<T>:
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,
})| Parameter | Default | Description |
|---|---|---|
callback | required | Called when an event of type E is fired |
eventBus | global bus | Custom EventBus to listen on (optional) |
exclusive | false | When 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 firstExclusive 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 state | Subscription state |
|---|---|
initState called | on<E>() registers listener |
| Widget is mounted | Callbacks are delivered |
| Widget is disposed | All 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
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
| Scenario | Use |
|---|---|
| Widget reacts to global state changes (theme, auth, settings) | ModularEventMixin |
| Widget drives navigation in response to business events | ModularEventMixin |
| Multiple widgets share the same event subscription | ModularEventMixin (one per widget) |
| Business logic reacts to events (not UI) | EventModule |
| Event must be handled exactly once across all modules | EventModule with exclusive: true |