Navigation is a core part of any mobile app. In a clean BLoC architecture, navigation should be treated as a side effect of state changes. You never call Navigator directly inside a bloc; instead, you emit a state that the UI listens to, and the UI (or a dedicated navigation observer) handles the actual navigation. This keeps your business logic pure and testable.
Why Navigation in Bloc Should Be a Side Effect
- Separation of concerns – Bloc should only emit states; navigation is a UI concern.
- Testability – A bloc that emits a
NavigateToHomestate is easy to test; you don’t need to mockNavigator. - Reusability – The same bloc can be used with different navigation systems (e.g., different routers).
- Predictability – All state changes are explicit; navigation becomes a response to a state change.
Approach 1: Using BlocListener for Navigation
The most common and recommended way is to have your bloc emit a navigation state (e.g., LoginSuccess, NavigateToProfile), and use a BlocListener (or BlocConsumer) to perform the actual navigation when that state is emitted.
Approach 2: Using a Custom Navigation Service
For larger apps, you may want to centralize navigation logic. Create a navigation service that the UI can call. The bloc still emits states, but the listener calls the service.
Approach 3: Passing a Callback to the Bloc (Not Recommended)
Some developers inject a navigation callback into the bloc. This is generally discouraged because it mixes UI concerns into the business logic and makes testing harder. However, it can be used in very simple scenarios.
Passing Arguments to Navigation
When navigating, you often need to pass data to the next screen. With BLoC, you can include the data in the state (e.g., AuthSuccess contains the userId). The listener then passes that data as arguments when pushing the route.
Deep Linking and Initial Routes
For deep linking, you may need to decide the initial route based on authentication state. This can be handled by using a SplashScreen that checks the auth state and then conditionally navigates. Or you can set the initial route in MaterialApp based on a synchronous check of persisted auth data.
Testing Navigation in Bloc
Since navigation is a side effect, you don't test the actual Navigator calls in unit tests. Instead, you test that the bloc emits the expected states. The UI tests (widget tests) can verify that the BlocListener correctly triggers navigation.
Best Practices
- Use
BlocListenerfor navigation – It keeps navigation code separate from UI rebuilding. - Emit navigation states – Instead of calling
Navigatordirectly from the bloc, emit a state likeNavigateToHome. - Keep navigation logic in the UI layer – The bloc should not know about
Navigator. - Use a navigation service for complex routing – Centralizes navigation and makes it easier to manage.
- Pass arguments via state or route arguments – Include data in the state, then pass as arguments when navigating.
- Test only state emissions – Unit tests for blocs should not depend on
Navigator; verify the emitted states.
Common Mistakes
- ❌ Calling
Navigatorinside the bloc – Breaks separation of concerns and makes testing difficult. ✅ Emit a navigation state and let the UI handle it. - ❌ Using
BlocBuilderfor navigation – Can cause multiple navigations if the state is emitted multiple times. ✅ UseBlocListener(only runs once per state). - ❌ Not handling state transitions correctly – For example, navigating on every
AuthSuccessemission even if already on the target screen. ✅ Use conditional navigation or alistenWhento avoid duplicate pushes. - ❌ Forgetting to dispose
BlocListener– Not an issue;BlocListenerautomatically manages its subscription. - ❌ Passing navigation callbacks to the bloc – Creates tight coupling. ✅ Use the state‑driven approach.
Conclusion
Navigation in BLoC should be treated as a side effect of state changes. By using BlocListener and emitting navigation‑related states, you keep your business logic pure, your UI reactive, and your code testable. Whether you use a simple BlocListener or a full navigation service, the key is to separate the decision to navigate (in the bloc) from the actual navigation (in the UI).