skip to content

Declarative Router API

The Router widget rebuilds a Navigator's pages list from app state through a RouterDelegate and parses URLs with a RouteInformationParser. Interviewers ask why it exists: deep links and web history.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

Why does Flutter offer the declarative Router API (MaterialApp.router) alongside Navigator.push, and when does an app actually need it?

level: juniorimportance: must knowfreq 55%

answer

  1. the stack as a function of state
  2. a deep link should rebuild, not push
  3. browser back and forward on the web
  4. pages list instead of push calls
  5. most apps get it through go_router

basics

~20 s

The Router API derives the navigator's stack from app state: a URL is parsed into state, state builds Navigator.pages, and state is reported back as a URL. Apps need it for deep links that rebuild the stack and web history.

solid answer

~50 s

`Navigator.push` is imperative: each call adds one route, and the stack is whatever history of calls happened. That breaks down in two places. A deep link should show a specific stack, say docs home with the layout article on top, regardless of what was open, but imperative or named-route navigation can only push. And on the web the address bar and the browser's back and forward buttons need the app's location as data. The Router API, used through `MaterialApp.router`, fixes both: a `RouteInformationParser` turns a URL into a typed configuration, a `RouterDelegate` holds the state and builds a `Navigator` whose `pages` list is derived from it, and the delegate's `currentConfiguration` is turned back into a URL for browser history. Most teams use it through go_router rather than writing a delegate; plain `Navigator.push` remains fine for mobile-only apps without deep links.

code

dart · 34 lines
dart
import 'package:flutter/material.dart';

// Imperative: the stack is the history of calls.
void openLayoutImperatively(BuildContext context) {
  Navigator.push(
    context,
    MaterialPageRoute<void>(builder: (context) => const ArticleScreen(slug: 'layout')),
  );
}

// Declarative: the stack is derived from state.
List<Page<void>> pagesFor(String? openSlug) => [
      const MaterialPage<void>(key: ValueKey<String>('home'), child: DocsHome()),
      if (openSlug != null)
        MaterialPage<void>(
          key: ValueKey<String>('doc:$openSlug'),
          child: ArticleScreen(slug: openSlug),
        ),
    ];

class DocsHome extends StatelessWidget {
  const DocsHome({super.key});

  @override
  Widget build(BuildContext context) => const Scaffold(body: Center(child: Text('Docs')));
}

class ArticleScreen extends StatelessWidget {
  const ArticleScreen({super.key, required this.slug});
  final String slug;

  @override
  Widget build(BuildContext context) => Scaffold(body: Center(child: Text(slug)));
}

go deeper

for a junior

Recall that Navigator.push is imperative and the Router API builds the stack from state, which is what deep links and the web need.

for a middle

Explain the parts, parser, delegate, provider, back-button dispatcher, and how a URL becomes pages and pages become a URL.

for a senior

Show how pageless routes and reverse-chronological browser history behave in a Router app and how that shapes which screens get URLs.

for a principal

Decide when a hand-written delegate is justified over go_router, weighing control against maintenance for a web-facing product.

## Two ways to describe navigation Flutter has two navigation styles that share one `Navigator` widget: - **Imperative** (often called Navigator 1.0): code calls `Navigator.push` and `Navigator.pop`. The stack is the accumulated result of those calls. - **Declarative** (the **Router API**, often called Navigator 2.0): the app holds navigation **state**, and the stack is rebuilt from it as a list of `Page` objects passed to `Navigator.pages`, the same way a widget tree is rebuilt from state. The Router API is enabled by `MaterialApp.router` (or `CupertinoApp.router`, `WidgetsApp.router`), given a `routerConfig` or its parts directly. ## Problems the imperative style cannot solve 1. **Deep links that describe a stack.** A link to `/docs/layout` in a documentation app should show docs home with the layout article on top. Flutter's docs note that with named routes a deep link always pushes a new route regardless of where the user is, and that behaviour cannot be customized. 2. **Web history.** In a browser, users expect the address bar to show where they are and the back and forward buttons to move through that history. That requires the app to **report** its location after every change and to **accept** a location from the browser at any time. 3. **Navigation driven by state.** When signing out should remove every signed-in screen, it is simpler to derive the stack from "is the user signed in" than to issue the right sequence of pops. ## The moving parts | Part | Job | |---|---| | `RouteInformationProvider` | Supplies the current `RouteInformation` (a `uri` plus optional `state`) from the platform, e.g. the browser URL; defaults to `PlatformRouteInformationProvider` | | `RouteInformationParser<T>` | Turns `RouteInformation` into a typed configuration `T`, and back again via `restoreRouteInformation` | | `RouterDelegate<T>` | Holds navigation state, applies configurations in `setNewRoutePath`, builds the `Navigator`, and exposes `currentConfiguration` | | `BackButtonDispatcher` | Delivers the Android back button to the delegate's `popRoute` | | `RouterConfig<T>` | Bundles the four so they can be passed as one `routerConfig` | ## The loop in a documentation app 1. The user opens `https://example.dev/docs/layout` in a browser. 2. The provider hands `RouteInformation(uri: /docs/layout)` to the parser, which returns `ArticlePath('layout')`. 3. The delegate's `setNewRoutePath` stores `slug = 'layout'`; the Router rebuilds. 4. The delegate's `build` returns a `Navigator` with pages `[home, article(layout)]`. 5. When the user opens another article in-app, the delegate updates its state and calls `notifyListeners`; the Router asks `currentConfiguration`, the parser's `restoreRouteInformation` turns it into `/docs/animation`, and the browser gets a new history entry. 6. Pressing the browser's back button delivers `/docs/layout` again through the provider, and step 2 repeats. ## When you do not need it - A mobile-only app with no deep links, or deep links that just open one screen: `Navigator.push` with `MaterialPageRoute` is simpler and fully supported. - Result-returning screens (a picker, a confirmation) still use `push` and `pop` inside a Router-based app; those routes are **pageless**: they are not in the URL and are removed when the page beneath them is removed. ## Imperative vs declarative at a glance | Question | `Navigator.push` | Router API | |---|---|---| | Where is the stack defined? | in the sequence of calls made | in state, rebuilt as `pages` | | What does a deep link do? | pushes one more route | replaces the stack with what the URL describes | | Does the web address bar follow? | no | yes, via `currentConfiguration` and the parser | | Result-returning screens | natural: `await push` | still done with `push` on top of pages | | Boilerplate | minimal | parser, delegate, page list (or a package) | ## How teams use it in practice Writing a `RouterDelegate` and parser by hand is verbose, so Flutter's docs recommend a routing package such as **go_router**, which implements the Router API for you from a route table. Knowing the underlying parts still matters: it explains why pushed dialogs are not deep-linkable, why browser back is "reverse chronological" (it returns to the previous location the Router reported, even after in-app pops), and what a custom delegate must implement when a package does not fit.

  • Why is a dialog shown with showDialog not deep-linkable in a Router-based app?
    It is added as a pageless route, not as a `Page` in the delegate's `pages` list, so it is not part of the delegate's state or `currentConfiguration`. The URL cannot describe it, and when the page beneath it is removed, the pageless route is removed too.
  • What does 'reverse chronological' browser back mean for a Router app?
    The browser's back button returns to the previous location the Router reported, not to the route below in the navigator. If the user pops the article in-app and then presses browser back, the article location is delivered again and the article reappears on the stack.

A map with a 'you are here' pin versus a trail of footprints: Navigator.push leaves footprints you can only retrace, while the Router keeps a pin (the state) that anyone, including the browser, can move, and the map (the page stack) is redrawn from it.

saying these in an interview costs you the question

  • Thinks the Router API replaced Navigator.push entirely
  • Says named routes already handle deep links properly
  • Believes the Router is only relevant to web builds
  • Thinks go_router is unrelated to the Router API
  • Expects dialogs pushed with showDialog to appear in the URL
open as a page

In Flutter's Navigator.pages API, how do Page objects become routes, why do page keys matter, and what must onDidRemovePage do?

level: middleimportance: should knowfreq 36%

basics

~20 s

Each Page creates a route via createRoute, and the Navigator diffs the new pages list against the old one by runtimeType and key. Give each distinct screen a unique key, and remove the popped page from your state in onDidRemovePage.

open as a page

In Flutter's Router API, what do RouteInformationParser and RouterDelegate each do when a docs-app URL is opened and when the user navigates in-app?

level: middleimportance: should knowfreq 32%

basics

~20 s

On a URL, the parser's parseRouteInformation builds a typed configuration and the delegate's setNewRoutePath applies it. In-app, the delegate changes state and notifies; the Router reads currentConfiguration and the parser's restoreRouteInformation turns it into the new URL.

open as a page

A Flutter web docs app built on a custom RouterDelegate never updates the address bar, and the browser back button leaves the site; what is wrong and how do you fix it?

level: seniorimportance: should knowfreq 24%

basics

~20 s

The Router never reports route information, so the browser gets no history entries. Usually currentConfiguration or restoreRouteInformation still returns null, or state changes skip notifyListeners, or screens are opened with Navigator.push. Override both methods, notify, and put pages in state.

open as a page

In Flutter's Router API, how does a BackButtonDispatcher route the Android back button, and when do you need a ChildBackButtonDispatcher?

level: middleimportance: nice to knowfreq 16%

basics

~10 s

RootBackButtonDispatcher, the default for MaterialApp.router, receives Android back presses and calls the delegate's popRoute, usually maybePop on its navigator. A nested Router needs a ChildBackButtonDispatcher that calls takePriority to get presses first.

open as a page