In complex Flutter apps, navigation is more than just pushing screens – it involves route guards, nested navigation, deep linking, and state synchronization. By combining BLoC with a routing solution like go_router, you can build a declarative, testable navigation system that responds to your app’s state. This guide explores advanced route management techniques using BLoC.
Why Combine BLoC with Advanced Routing?
- Declarative navigation – Routes are defined based on the current state (e.g., show login if not authenticated).
- Route guards – Protect routes based on authentication or user roles without sprinkling checks everywhere.
- Deep linking – Handle URLs seamlessly, converting them to the correct app state.
- Testability – Navigation logic becomes part of the state machine, easy to test in isolation.
- Scalability – Manage complex nested navigation (tabs, drawers) with clean separation.
- Route Guards with go_router and BLoC
go_router is the recommended routing package for Flutter. It supports redirects based on conditions – perfect for route guards. You can integrate a BLoC to check authentication state and redirect accordingly.
In this example, the redirect function reads the current AuthBloc state and decides where to go. This centralises all route protection logic.
- Nested Navigation with BLoC and ShellRoute
For tab‑based navigation (e.g., bottom navigation bar), you can use ShellRoute to define a common shell that stays persistent. The selected tab can be controlled by a BLoC that emits the current tab index.
- Deep Linking with BLoC
Deep links (URLs) can be handled by go_router which parses the URL and builds the corresponding route. You still need to ensure the app is in the correct state (e.g., user logged in) before navigating. The redirect function already handles this. For dynamic parameters, use path parameters.
To test deep linking on Android/iOS, you need to configure the app's manifest/Info.plist. With go_router, the deep link URL will be automatically matched and the router will handle it.
- Using BLoC with go_router for Authentication Flow
Here’s a complete example of an authentication flow using BLoC and go_router, including a splash screen that checks the auth state.
- Testing Route Guards and Navigation
Test your route logic by setting up a test environment with a mock AuthBloc and checking the router’s behavior. You can use go_router’s MaterialApp.router and test navigation using tester.pumpAndSettle and verifying the current route.
Best Practices
- Use go_router with BLoC – It’s the most robust solution for complex navigation, supporting redirects, nested routes, and deep linking.
- Centralise route guards – Put all protection logic in the
redirectfunction, not scattered across screens. - Keep navigation state in BLoC – For tab selection, drawer state, etc., use a BLoC to manage them.
- Test navigation separately – Use widget tests to verify that the correct screens appear given the BLoC state.
- Handle deep links gracefully – Ensure your app can navigate to the correct content even if the user isn’t logged in (by queuing the intended route after login).
- Use path parameters for IDs – Makes deep linking easier and URLs cleaner.
Common Mistakes
- ❌ Using
Navigator.pushinside BLoC – Bypasses the routing system and makes guards ineffective. ✅ Let the router handle navigation; usecontext.go()orrouter.push(). - ❌ Not handling
AuthInitial– The app may show a blank screen or incorrect route while checking auth. ✅ Redirect to a splash screen or show a loader. - ❌ Hardcoding routes in multiple places – Leads to maintenance issues. ✅ Use route constants and a single source of truth.
- ❌ Forgetting to provide BLoC to the router – The router’s
redirectcan’t access the bloc. ✅ Wrap the app withBlocProvideror useProviderabove the router. - ❌ Mixing navigation with business logic – Navigation decisions should be based on state, not internal logic of blocs. ✅ Let the router decide based on the current state.
Conclusion
Combining BLoC with a declarative routing solution like go_router unlocks powerful, maintainable navigation for Flutter apps. Route guards become a simple redirect function, nested navigation is easily handled with ShellRoute, and deep linking works out of the box. By keeping navigation logic separate from business logic and relying on state, you build apps that are both robust and testable.