In Bloc-based applications, navigation is a side effect that should be triggered by state changes. But what if you want to explicitly tie navigation to a specific event? Using navigation events – events that represent the intention to navigate – can make your navigation flow more explicit and traceable. This guide covers how to implement event-driven navigation with Flutter Bloc.
The Navigation Event Pattern
Instead of having a bloc emit a generic state (like LoggedIn) and then listening for it to navigate, you can define dedicated events that represent navigation intents. The bloc processes these events and may or may not emit additional states. The UI listens for these navigation events (or dedicated navigation states) and performs the actual navigation.
Approach 1: Dedicated Navigation States
Define states that represent navigation actions, and emit them from your bloc. This keeps the navigation intent in the state stream.
Approach 2: Events as Navigation Triggers (without dedicated states)
Sometimes you don't need to change the application state; you just want to navigate. In that case, you can add events that are handled directly in the UI via BlocListener without emitting a state. This keeps the bloc's state focused on business data.
Approach 3: Navigation Events as Part of Composite Events
Sometimes a single user action should trigger both business logic and navigation. You can use a composite event that includes navigation intent as part of the state.
Handling Multiple Navigation Events
When a bloc can emit different navigation states, you can centralise navigation handling in a single BlocListener or use multiple listeners for clarity.
Resetting Navigation State After Use
To prevent repeated navigation (e.g., if the widget rebuilds), it's important to reset the navigation state after it has been handled. You can emit a neutral state or clear the navigation flag.
Testing Navigation Events
When testing navigation events, you can verify that the correct navigation state is emitted, or use a mock navigator to verify that navigation actually occurs.
Best Practices
- Use navigation states, not events – Emit a dedicated state to trigger navigation; it's easier to test and trace.
- Reset navigation flags – Always clear navigation intents after they've been handled to avoid duplicate navigation.
- Keep navigation states minimal – Include only what's needed to perform the navigation (route name, arguments).
- Separate business states from navigation states – Don't mix them unless necessary; use composition or dedicated state classes.
- Use a single
BlocListenerfor navigation – Centralise all navigation logic in one place for a given bloc, or use multiple listeners if you prefer separation. - Prefer
pushReplacementwhen appropriate – Avoid piling up the navigation stack unnecessarily.
Common Mistakes
- ❌ Not resetting navigation state – Causes navigation to occur repeatedly on every rebuild.
- ❌ Using events directly for navigation – Without emitting a state, you lose traceability and testing capabilities.
- ❌ Mixing navigation logic with business logic in the bloc – Keep navigation intent separate from core state.
- ❌ Using
Navigatorinside the bloc – Makes bloc hard to test and couples it to Flutter. - ❌ Forgetting to handle back navigation – When using
pushReplacement, consider if the user should be able to go back.
What's Next?
Now that you understand event-driven navigation, explore more advanced patterns like deep linking with Bloc and integrating with GoRouter.
Next, explore Bloc navigation with GoRouter and Bloc testing.