Navigator 2.0 Migration Guide: From Anonymous Routes to the Router
A practical path from Navigator 1.0 push/pop to RouterDelegate, RouteInformationParser, and deep-linkable Page objects.
Published on • October 20, 2026
AI Assistant

Navigator 1.0 — push, pop, anonymous MaterialPageRoutes — works fine until you ship a web build or need deep links to reconstruct state. Then the imperative stack fights you. Flutter’s navigation and routing docs now draw a sharp line: small apps use the Navigator; apps with deep linking or navigation requirements should use the Router. This is a migration path from one to the other.
Why migrate at all
The docs list concrete limitations of the named-route approach (the usual middle step):
- Deep link behavior “can’t be customized” — when a platform deep link arrives, Flutter pushes a new
Routeregardless of where the user currently is. Deep-link into an app three screens deep and you get a fourth screen on top. - The browser forward button isn’t supported for apps using named routes.
The official recommendation today: don’t use named routes for most applications. Use go_router or Navigator with MaterialPageRoute. If you need full control, use the Router APIs directly.
Step 1: inventory your navigation
Audit three things before touching code:
- Anonymous pushes —
Navigator.of(context).push(MaterialPageRoute(...))calls. Each constructs a route from an inline builder, so nothing about it is reconstructible from a URL. - Global navigation keys and
pushNamed/pushNamedAndRemoveUntil— these encode navigation as imperative commands, not state. - Navigation side effects — redirects after login, guards, analytics hooks scattered across call sites.
Step 2: decide your target
You have two legitimate destinations:
A routing package (go_router). The docs’ first recommendation. Declarative routes, the router constructor on MaterialApp, and context.go('/second') for navigation. You get deep linking, the browser History API, and redirect guards without writing parser code.
Hand-rolled Router. The docs’ “advanced developers” path: override RouteInformationParser and RouterDelegate, and control the screen stack precisely by supplying a list of Page objects through Navigator.pages. The router constructor on MaterialApp accepts the resulting configuration:
class AppRouterDelegate extends RouterDelegate<String>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<String> {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
String? selectedId;
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
const MaterialPage(child: HomeScreen()),
if (selectedId != null)
MaterialPage(child: DetailScreen(id: selectedId!)),
],
onDidRemovePage: (page) {
selectedId = null;
notifyListeners();
},
);
}
@override
Future<void> setNewRoutePath(String configuration) async {
selectedId = configuration;
}
}
class AppRouteInformationParser extends RouteInformationParser<String> {
@override
Future<String> parseRouteInformation(RouteInformation routeInformation) async =>
routeInformation.uri.path;
@override
RouteInformation? restoreRouteInformation(String configuration) =>
RouteInformation(uri: Uri.parse(configuration));
}
Wire them into the app:
MaterialApp.router(
routerDelegate: routerDelegate,
routeInformationParser: parser,
)
Routing packages like go_router wrap exactly these classes — reading them explains every abstraction in the package.
Step 3: convert pushes into pages
The core conceptual shift: stop commanding the navigator and start describing it. State owns the route set; the build method renders it as Page objects. Every anonymous push becomes a state mutation plus a page appearing in the list.
Understand the page-backed/pageless distinction the docs define, because it governs deep linking:
- Routes created from
Pageobjects viaNavigator.pagesare page-backed and always deep-linkable. - Routes created by
Navigator.pushorshowDialogare pageless — not deep-linkable. - When a page-backed route is removed, all pageless routes after it are removed too.
- You can’t block navigation away from page-backed screens with
PopScope— that concern moves to your routing layer (consult your package’s docs).
Dialogs and bottom sheets legitimately stay pageless; that’s fine.
Step 4: get deep linking right
With the Router in place, deep-link behavior changes from “push whatever arrived” to “parse the URL and configure the page list accordingly.” The docs’ behavior table shows why this matters: with Router, the incoming path goes through RouteInformationParser and RouterDelegate.setNewRoutePath, which configures the Navigator with the corresponding Pages — including replacing the current set while the app is running. With the old Navigator approach, pushRoute just stacks a screen.
Two migration details from the deep linking page:
- If you previously wrote a plugin to handle deep links, opt out of Flutter’s built-in handler: set
FlutterDeepLinkingEnabledtofalseinInfo.plistorflutter_deeplinking_enabledtofalseinAndroidManifest.xml. - On web, no extra setup is needed — route paths work like iOS/Android deep links. Default reads the path from the URL fragment (
/#/path), changeable via the URL strategy config.
Step 5: verify the browser contract
Router-integrated apps add a History API entry on every navigation, and the back button uses reverse chronological navigation: pop a page, press browser back, and the page is pushed back on. Test this deliberately — it’s the behavior named routes couldn’t support and the main reason web users notice the difference.
Migration tactics that reduce risk
- Migrate route by route. Page-backed and pageless routes coexist on one
Navigator, so you can convert your deep-linkable screens first and leave leaf dialogs alone. - Move guards into the parser. Auth redirects belong in
parseRouteInformation/setNewRoutePath, not scatteredpushReplacementcalls. - Keep one navigation API. Mixed
push+context.goin the same feature is where state desync bugs come from.
The Router API design doc (flutter.dev/go/navigator-with-router) and the “Learning Flutter’s new navigation and routing system” blog post go deeper on the raw API; the docs point most app teams at go_router for good reason.