Choosing Between Flutter Navigator 1.0 and Navigator 2.0 for Routing
Decide when to use Flutter's Navigator 1.0 imperative API versus Navigator 2.0 declarative router based on deep‑linking, web URL sync, and boilerplate tolerance.
07 Jul 2025, 01:07 UTC

Problem and Takeaway
When building a medium‑to‑large Flutter app you must decide how to handle navigation. The choice influences deep‑link support, web URL synchronisation, and the amount of boilerplate you write. The useful takeaway is: adopt Navigator 2.0 when you need declarative URL‑state coupling, multiple nested navigators, or reliable web deep linking; otherwise Navigator 1.0 keeps the code simple for linear flows.
Decision Constraints
- Flutter SDK ≥ 2.5 (stable RouterDelegate API)
- Willingness to implement a RouterDelegate, RouteInformationParser and optionally a RouteInformationParserDelegate
- Acceptance of extra boilerplate for the router layer
Option Comparison
| Aspect | Navigator 1.0 | Navigator 2.0 |
|---|---|---|
| API style | Imperative push/pop | Declarative router & parser |
| Deep linking | Manual handling in UI code | Built‑in via RouteInformationParser |
| Web URL sync | Limited, requires manual listeners | Automatic with browser URL |
| Boilerplate | Low (just Navigator widget) | Higher (RouterDelegate, parser, delegate) |
| Learning curve | Low | Moderate |
Trade‑offs
Navigator 2.0 gives strong URL‑state coupling, which simplifies deep linking and keeps the browser address bar in sync with the navigation stack. This is valuable for apps that target the web or need complex nested navigation. The downside is added code complexity and a steeper learning curve; you must maintain the router delegate’s state and parser logic. Navigator 1.0 remains ideal for prototypes, apps with only a few linear screens, or teams that prefer minimal routing code, but it forces you to write custom deep‑link handling and offers limited web integration.
Concrete Navigator 2.0 Implementation
Define the app state
class AppState { final String page; final String? id; AppState({required this.page, this.id}); }Route information parser
class AppRouteInformationParser extends RouteInformationParser<AppState> { @override Future<AppState> parseRouteInformation(RouteInformation routeInformation) async { final uri = Uri.parse(routeInformation.location ?? ''); if (uri.pathSegments.isEmpty || uri.pathSegments.first == '') { return AppState(page: 'home'); } if (uri.pathSegments.length == 2 && uri.pathSegments.first == 'details') { return AppState(page: 'details', id: uri.pathSegments[1]); } return AppState(page: 'home'); } @override RouteInformation restoreRouteInformation(AppState state) { if (state.page == 'details' && state.id != null) { return RouteInformation(location: '/details/${state.id}'); } return RouteInformation(location: '/'); } }Router delegate
class AppRouter extends RouterDelegate<AppState> with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppState> { final GlobalKey<NavigatorState> _navigatorKey; AppState _state = AppState(page: 'home'); AppRouter() : _navigatorKey = GlobalKey<NavigatorState>(); @override GlobalKey<NavigatorState> get navigatorKey => _navigatorKey; @override AppState get currentConfiguration => _state; @override Future<void> setNewRoutePath(RouteInformation routeInformation) async { try { _state = await AppRouteInformationParser().parseRouteInformation(routeInformation); notifyListeners(); } catch (e) { } } @override Widget build(BuildContext context) { return Navigator( key: _navigatorKey, pages: [ MaterialPage(key: ValueKey('home'), child: HomeScreen()), if (_state.page == 'details' && _state.id != null) MaterialPage(key: ValueKey('details-${_state.id}'), child: DetailsScreen(id: _state.id!)), ], onPopPage: (route, result) { if (!route.didPop(result)) return false; _state = AppState(page: 'home'); notifyListeners(); return true; } ); }Wire it up
MaterialApp.router( routeInformationParser: AppRouteInformationParser(), routerDelegate: AppRouter(), )Validation Steps
- Run
flutter doctorand verify Flutter ≥ 2.5 and that Chrome or an emulator is available. - Start the app:
flutter run -d chrome(or-d androidfor an emulator). - Use the UI to navigate to a detail page; observe that the address bar updates to something like
http://localhost:port/details/42. - Paste a URL such as
http://localhost:port/details/10into the browser’s address bar and press Enter; the DetailsScreen with id 10 should appear. - Optionally add a widget test:
A passing test confirms that the parser and delegate correctly interpret the route.tester.pumpWidget(MaterialApp.router( routeInformationParser: AppRouteInformationParser(), routerDelegate: AppRouter(), )); await tester.tap(find.text('Go to Details')); await tester.pumpAndSettle(); expect(find.text('Details'), findsOneWidget);
Limitations and Practical Check
If your app only has two or three screens and does not require web deep linking, the extra classes introduced by Navigator 2.0 may add unnecessary complexity. In that case, staying with Navigator 1.0 reduces boilerplate and keeps the learning curve low. To verify that you have chosen the right approach, run the app on the web, attempt a deep link (e.g., open https://example.com/details/99), and confirm the correct screen appears without any manual route‑parsing code in your UI layer. If the screen shows correctly and the URL stays in sync when you navigate programmatically, the Navigator 2.0 setup is working as intended.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.