Navigator 2.0 and the Router API
Navigator 2.0 and the Router API
Flutter's original Navigator (often called Navigator 1.0) is a simple imperative stack: you push routes and pop them. That model works well for most apps, but it breaks down the moment the host platform tries to tell Flutter what to show—for example, when a user deep-links to /products/42 on the web, or when the OS back button should pop only part of a nested stack.
Navigator 2.0 (also called the Router API) solves this by making navigation declarative. Instead of imperatively pushing pages, you maintain an app-state object that describes the current navigation intent, and a set of collaborating classes derives the correct page stack from that state—automatically, every time the state changes.
go_router, auto_route, and beamer build on top of. Learning the raw API gives you deep insight into how those packages work and when to drop down to the primitives.The Four Collaborating Pieces
The Router API is composed of four interacting classes:
- RouteInformationProvider — Supplies a
RouteInformation(a URI + state blob) from the platform. The default implementation,PlatformRouteInformationProvider, listens to the browser's address bar on web and to Android's intent system on mobile. - RouteInformationParser — Converts a raw
RouteInformationinto your typed app-state object (e.g. aAppRoutePathenum or class). This is where URL parsing lives. - RouterDelegate — The heart of the system. It holds the current app-route state, builds the
Navigatorwidget with the correctpageslist, and reports the current URL back to the platform viacurrentConfiguration. - BackButtonDispatcher — Intercepts the platform back button and dispatches it to the active delegate. The default
RootBackButtonDispatcherworks for most cases.
Defining Your App Route State
Start by modelling what your app can show as a plain Dart class. Keep it immutable and comparable.
App Route Path Model
// Represents every "address" the app can be at
class AppRoutePath {
final bool isHomePage;
final int? productId; // non-null when on ProductDetailPage
final bool isUnknown;
const AppRoutePath.home()
: isHomePage = true,
productId = null,
isUnknown = false;
const AppRoutePath.productDetail(this.productId)
: isHomePage = false,
isUnknown = false;
const AppRoutePath.unknown()
: isHomePage = false,
productId = null,
isUnknown = true;
bool get isProductDetail => productId != null;
}
Implementing RouteInformationParser
The parser has two responsibilities: parse an incoming URL into your AppRoutePath, and restore an AppRoutePath back into a RouteInformation so the browser bar stays in sync.
AppRouteInformationParser
class AppRouteInformationParser
extends RouteInformationParser<AppRoutePath> {
@override
Future<AppRoutePath> parseRouteInformation(
RouteInformation routeInformation) async {
final uri = Uri.parse(routeInformation.uri.toString());
// Handle "/" => HomePage
if (uri.pathSegments.isEmpty) {
return const AppRoutePath.home();
}
// Handle "/products/:id" => ProductDetailPage
if (uri.pathSegments.length == 2 &&
uri.pathSegments[0] == 'products') {
final id = int.tryParse(uri.pathSegments[1]);
if (id != null) return AppRoutePath.productDetail(id);
}
return const AppRoutePath.unknown();
}
@override
RouteInformation restoreRouteInformation(AppRoutePath path) {
if (path.isHomePage) {
return RouteInformation(uri: Uri.parse('/'));
}
if (path.isProductDetail) {
return RouteInformation(
uri: Uri.parse('/products/${path.productId}'));
}
return RouteInformation(uri: Uri.parse('/404'));
}
}
Implementing RouterDelegate
The delegate extends RouterDelegate<AppRoutePath> and mixes in ChangeNotifier (so the Router widget can listen for state changes) and PopNavigatorRouterDelegateMixin (so the system back button pops correctly).
AppRouterDelegate — building the page stack from state
class AppRouterDelegate extends RouterDelegate<AppRoutePath>
with ChangeNotifier, PopNavigatorRouterDelegateMixin<AppRoutePath> {
@override
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
// ---- mutable app-route state ----
int? _selectedProductId;
bool _show404 = false;
// Public setters that update state and notify the Router
void selectProduct(int id) {
_selectedProductId = id;
_show404 = false;
notifyListeners(); // triggers Router to call build() again
}
void clearSelection() {
_selectedProductId = null;
_show404 = false;
notifyListeners();
}
// ---- currentConfiguration: tells the platform what URL to show ----
@override
AppRoutePath get currentConfiguration {
if (_show404) return const AppRoutePath.unknown();
if (_selectedProductId != null) {
return AppRoutePath.productDetail(_selectedProductId!);
}
return const AppRoutePath.home();
}
// ---- setNewRoutePath: called when the platform sends a new URL ----
@override
Future<void> setNewRoutePath(AppRoutePath path) async {
if (path.isUnknown) {
_selectedProductId = null;
_show404 = true;
} else if (path.isProductDetail) {
_selectedProductId = path.productId;
_show404 = false;
} else {
_selectedProductId = null;
_show404 = false;
}
// No notifyListeners() here — Router calls build() automatically
}
// ---- build: the page stack is pure function of state ----
@override
Widget build(BuildContext context) {
return Navigator(
key: navigatorKey,
pages: [
// Home is always on the stack
MaterialPage(
key: const ValueKey('HomePage'),
child: HomePage(
onProductSelected: selectProduct,
),
),
// Detail page is conditionally on top
if (_selectedProductId != null)
MaterialPage(
key: ValueKey('ProductDetailPage-$_selectedProductId'),
child: ProductDetailPage(
productId: _selectedProductId!,
onBack: clearSelection,
),
),
// 404 overlays everything
if (_show404)
const MaterialPage(
key: ValueKey('UnknownPage'),
child: UnknownPage(),
),
],
onPopPage: (route, result) {
if (!route.didPop(result)) return false;
clearSelection();
return true;
},
);
}
}
Wiring It All Together in MaterialApp.router
Pass your delegate and parser to MaterialApp.router. The framework wires up the RouteInformationProvider automatically on web; on mobile it uses a no-op provider.
main.dart
void main() => runApp(const MyApp());
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _delegate = AppRouterDelegate();
final _parser = AppRouteInformationParser();
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Navigator 2.0 Demo',
routerDelegate: _delegate,
routeInformationParser: _parser,
// Optional: supply your own provider for testing
// routeInformationProvider: ...,
);
}
}
RouterDelegate and RouteInformationParser in a State object or use a DI solution—never create them inside build(). Recreating them on every rebuild discards the navigation state and causes the app to reset to the home route.Key Advantages Over Navigator 1.0
- Deep-link support — the platform can hand any URL to
parseRouteInformationand the correct page stack is built declaratively. - Back-forward web navigation —
restoreRouteInformationkeeps the browser history bar in sync. - Testable — the page stack is a pure function of
AppRoutePath; you can unit-test routing logic without aWidgetTester. - Fine-grained back button control — nested delegates via
ChildBackButtonDispatcherlet inner navigators intercept the back button.
Summary
Navigator 2.0 replaces the imperative push/pop model with a declarative one: you update an app-state object; the RouterDelegate rebuilds the Navigator page stack from it; the RouteInformationParser converts URLs to state and back. The four pieces—RouteInformationProvider, RouteInformationParser, RouterDelegate, and BackButtonDispatcher—work together to give Flutter first-class deep-linking and browser-history support. Production apps typically use a package like go_router that wraps these primitives, but understanding the raw API makes you a far more effective Flutter developer.