skip to content

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