Why Switch to Flutter Navigator 2.0? A Practical Guide to Declarative Routing
Navigator 2.0 lets you model navigation as a list of pages, simplifying deep‑linking and state restoration. This guide compares it to Navigator 1.0, walks through a two‑screen example, discusses trade‑offs, and gives concrete next steps for developers.
16 Apr 2026, 23:23 UTC

Problem
Most Flutter apps still use the legacy Navigator API (Navigator 1.0) with imperative push and pop calls. That model works fine for simple flows, but it becomes brittle when you need deep‑linking, state restoration, or complex onboarding sequences. Developers often end up scattering navigation logic across widgets, making the code hard to test and maintain.
Thesis
Navigator 2.0 (declarative routing) lets you model the entire navigation stack as a list of Page objects. By moving navigation state into a data model, you gain:
- Built‑in deep‑link handling and state restoration.
- Cleaner separation between UI and navigation logic.
- Better integration with state‑management solutions.
Navigator 1.0 vs Navigator 2.0
Navigator 1.0 is a stack that you manipulate imperatively. The router is hidden behind the MaterialApp widget, and you only react to user actions.
Navigator 2.0 exposes two core classes:
RouteInformationParser– turns a URL string into a custom data model.RouterDelegate– builds theNavigatorbased on that model.
Because navigation state lives in a data model, you can:
- Update the stack by changing a list of
Pageobjects. - Persist the stack automatically (state restoration).
- Respond to external events (e.g., deep links) without imperative calls.
Worked Example: Two‑Screen Flow with Deep Linking
Below is a minimal, self‑contained example that demonstrates a two‑screen app using Navigator 2.0. The code is meant to be dropped into a fresh flutter create project.
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routeInformationParser: MyRouteInformationParser(),
routerDelegate: MyRouterDelegate(),
title: 'Navigator 2.0 Demo',
);
}
}
// Simple route data model
class RoutePath {
final String? screen;
RoutePath.home() : screen = null;
RoutePath.details() : screen = 'details';
}
class MyRouteInformationParser extends RouteInformationParser {
@override
Future parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.location ?? '');
if (uri.path == '/details') return RoutePath.details();
return RoutePath.home();
}
}
class MyRouterDelegate extends RouterDelegate
with ChangeNotifier, PopNavigatorRouterDelegateMixin {
@override
final GlobalKey navigatorKey = GlobalKey();
// The current stack of pages
List _pages = [
MaterialPage(key: ValueKey('Home'), child: HomeScreen()),
];
@override
RoutePath get currentConfiguration {
return _pages.last is MaterialPage && _pages.last.child is DetailsScreen
? RoutePath.details()
: RoutePath.home();
}
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: List.of(_pages),
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
_pages.removeLast();
notifyListeners();
return true;
},
);
}
// Public API for navigation
void goToDetails() {
_pages.add(MaterialPage(key: ValueKey('Details'), child: DetailsScreen()));
notifyListeners();
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
final delegate = Router.of(context).routerDelegate as MyRouterDelegate;
return Scaffold(
appBar: AppBar(title: Text('Home')),
body: Center(
child: ElevatedButton(
onPressed: delegate.goToDetails,
child: Text('Go to Details'),
),
),
);
}
}
class DetailsScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Details')),
body: Center(child: Text('This is the details screen')),
);
}
}
Key points to verify:
- Running
flutter runshows a home screen with a button. Pressing the button pushes the details screen. - Closing the app and re‑opening restores the last screen automatically.
- Launching the app with a URL like
myapp://details(configured inAndroidManifest.xmlorInfo.plist) directly opens the details screen.
Trade‑offs and Limitations
While Navigator 2.0 offers powerful features, it comes with overhead:
- Boilerplate – You must implement
RouteInformationParserandRouterDelegate, which can be intimidating for small projects. - Learning Curve – Transitioning from imperative
push/popto a declarative list ofPageobjects requires a mindset shift. - Transition Customization – Custom page transitions are more involved; you need to provide
Pagesubclasses or usePageRouteBuilderinside the delegate. - Third‑party Packages – Some routing packages (e.g.,
auto_route) abstract away the boilerplate, but they add another dependency layer.
For apps with a flat navigation structure (e.g., a single‑page form), Navigator 1.0 remains a pragmatic choice. The declarative model shines when you need:
- Deep‑linking to arbitrary screens.
- State restoration across app restarts.
- Complex onboarding flows with optional steps.
- Integration with global state managers (e.g.,
Provider,Bloc) where navigation state can be shared.
Actionable Next Steps
- Audit your navigation – List all screens, transitions, and any deep‑linking requirements.
- Prototype a small feature – Replace a single
pushcall with aPageaddition in aRouterDelegateto feel the declarative flow. - Use a helper package – Consider
go_routerorauto_routeif you want to reduce boilerplate while still leveraging Navigator 2.0. - Integrate state restoration – Verify that
RestorationMixinis enabled inMaterialApp.routerand that yourRouterDelegatepersists the page stack. - Iterate gradually – Migrate one navigation module at a time instead of a wholesale rewrite.
By following these steps, you can evaluate whether the benefits of Navigator 2.0 outweigh its complexity for your project.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.