Navigation & Routing

Navigator 2.0 and the Router API

18 min Lesson 8 of 14

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.

Note: Navigator 2.0 is the foundation that packages such as 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 RouteInformation into your typed app-state object (e.g. a AppRoutePath enum or class). This is where URL parsing lives.
  • RouterDelegate — The heart of the system. It holds the current app-route state, builds the Navigator widget with the correct pages list, and reports the current URL back to the platform via currentConfiguration.
  • BackButtonDispatcher — Intercepts the platform back button and dispatches it to the active delegate. The default RootBackButtonDispatcher works for most cases.
Tip: Think of the flow as a loop: Platform URL → Parser → app state → RouterDelegate → Navigator pages → rendered UI → user action → new app state → Parser serialises new URL back to platform.

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: ...,
    );
  }
}
Warning: Always store 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 parseRouteInformation and the correct page stack is built declaratively.
  • Back-forward web navigationrestoreRouteInformation keeps the browser history bar in sync.
  • Testable — the page stack is a pure function of AppRoutePath; you can unit-test routing logic without a WidgetTester.
  • Fine-grained back button control — nested delegates via ChildBackButtonDispatcher let 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.