Mastering Flutter Navigator 2.0: Declarative Routing for Deep Linking and State Restoration
Navigator 2.0 replaces push/pop with a URL‑driven page stack. This guide shows how to build a Router that maps URLs to Page objects, enable deep linking, and restore state, plus common pitfalls.
14 Jul 2026, 08:40 UTC

Why Navigator 2.0 Matters
Flutter’s original Navigator used an imperative push/pop API that worked well for simple flows but made deep linking, state restoration, and custom transition logic cumbersome. Navigator 2.0 introduces a declarative model: a Router widget that owns a RouteInformationParser and a RouterDelegate. Together they expose a URL‑driven page stack, making web and mobile navigation behave the same and simplifying complex navigation patterns.
Declarative Routing in Action
The core idea is that the UI reflects the navigation state. Instead of calling Navigator.push(context, …), you rebuild a list of Page objects that represent the current stack. The Router widget then renders those pages and updates the browser address bar (on web) or the system back‑stack (on mobile).
Minimal Example: Home & Profile
Below is a self‑contained example that maps the URLs /home and /profile to two pages. It demonstrates how to navigate programmatically without calling Navigator.push.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
// Simple enum to represent our routes.
enum AppRoute { home, profile }
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Navigator 2.0 Demo',
routerDelegate: AppRouterDelegate(),
routeInformationParser: AppRouteParser(),
backButtonDispatcher: RootBackButtonDispatcher(),
);
}
}
/// Parses the URL string into an AppRoute value.
class AppRouteParser extends RouteInformationParser {
@override
Future parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '/');
switch (uri.path) {
case '/profile':
return AppRoute.profile;
case '/':
case '/home':
default:
return AppRoute.home;
}
}
@override
RouteInformation restoreRouteInformation(AppRoute configuration) {
return RouteInformation(
location: configuration == AppRoute.profile ? '/profile' : '/home');
}
}
/// Builds the list of Page objects that represent the stack.
class AppRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
final GlobalKey navigatorKey = GlobalKey();
AppRoute _currentRoute = AppRoute.home;
AppRoute get currentConfiguration => _currentRoute;
@override
Widget build(BuildContext context) {
List pages = [];
pages.add(MaterialPage(
key: ValueKey('HomePage'),
child: HomePage(onNavigate: () => _setRoute(AppRoute.profile)),
));
if (_currentRoute == AppRoute.profile) {
pages.add(MaterialPage(
key: ValueKey('ProfilePage'),
child: ProfilePage(),
));
}
return Navigator(
key: navigatorKey,
pages: pages,
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
_setRoute(AppRoute.home);
return true;
},
);
}
void _setRoute(AppRoute route) {
_currentRoute = route;
notifyListeners();
}
@override
Future setNewRoutePath(AppRoute configuration) async {
_currentRoute = configuration;
}
}
class HomePage extends StatelessWidget {
final VoidCallback onNavigate;
HomePage({required this.onNavigate});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: ElevatedButton(
child: Text('Go to Profile'),
onPressed: onNavigate,
),
),
);
}
}
class ProfilePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Center(child: Text('This is the profile page')),
);
}
}
How It Works
- RouteInformationParser converts the browser URL (or deep‑link intent) into an
AppRoutevalue. - RouterDelegate holds the current
AppRouteand rebuilds thepageslist whenever the state changes. TheNavigatorwidget inside the delegate renders those pages. - When the user taps the button, the delegate’s
_setRoutemethod updates the state andnotifyListeners()triggers a rebuild. The URL bar automatically updates to/profilebecause the delegate’scurrentConfigurationreflects the new route. - Back navigation is handled by
onPopPage, which resets the state tohomeand updates the URL accordingly.
State Restoration
Navigator 2.0 preserves the page stack across app restarts. When the app is terminated and reopened, the Router will receive the last known RouteInformation from the platform. The AppRouterDelegate’s setNewRoutePath method will be called automatically, rebuilding the stack to match the previous state.
Practical Verification Steps
- Run the app in a browser. The URL should show
/home. - Click the button to navigate to Profile. Observe the URL changes to
/profileand the new page appears. - Press the browser back button. The URL reverts to
/homeand the Home page reappears. - Close the browser tab and reopen it. The app should start on the Profile page if that was the last visited route, demonstrating state restoration.
Common Pitfalls & Limitations
- Boilerplate Overhead: Navigator 2.0 introduces a larger API surface. For simple one‑screen apps, the legacy
Navigator.pushis still easier. - Debugging Complexity: The page list is no longer implicit. Errors such as duplicate keys or missing
onPopPagehandling can be harder to trace. - Performance Considerations: Rebuilding the entire
pageslist on every state change is cheap, but if you rebuild it on every frame (e.g., inside abuildmethod that callssetStatecontinuously), you may see frame drops. - Unknown Routes: If the parser receives an unrecognized path, you must decide whether to redirect to a fallback page or show an error. Forgetting to handle this can leave the app in an inconsistent state.
- Custom Transitions: The default
MaterialPageuses a platform‑specific transition. For unique animations, subclassPageor usePageRouteBuilderand remember to supply a uniquekeyto avoid transition glitches. - Back Button Handling: On Android, the system back button triggers
onPopPage. If you overridePopNavigatorRouterDelegateMixinincorrectly, the back button may close the app instead of navigating back.
When to Choose Navigator 2.0
Adopt Navigator 2.0 when:
- You need deep linking that works identically on web and mobile.
- State restoration across app restarts is critical.
- Custom navigation logic (e.g., authentication gating, role‑based flows) requires a declarative stack.
- You plan to use a state‑management solution (Provider, Riverpod, Bloc) that can rebuild the route stack in response to global state changes.
Conclusion
Navigator 2.0 gives you a URL‑driven, declarative navigation model that aligns Flutter’s web and mobile experiences. While it adds some boilerplate, the benefits—consistent deep linking, effortless state restoration, and flexible transition logic—make it the recommended approach for medium to large apps. Start by adding a Router to your project, map a few routes, and watch the browser address bar stay in sync with your page stack.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.