skip to content

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%

answer

  1. URL to typed config, config to URL
  2. parseRouteInformation then setNewRoutePath
  3. notifyListeners makes the Router rebuild
  4. currentConfiguration and restoreRouteInformation
  5. SynchronousFuture when no async work

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.

solid answer

~40 s

The two run in opposite directions. **Inbound**: the route information provider supplies a `RouteInformation` whose `uri` is, say, `/docs/layout`; the `RouteInformationParser.parseRouteInformation` turns it into a typed configuration such as `ArticlePath('layout')`; the Router passes that to `RouterDelegate.setNewRoutePath`, which updates the delegate's state, and after it completes the Router rebuilds and the delegate's `build` returns a `Navigator` with the matching pages. **Outbound**: when the user taps another article, the delegate changes its own state and calls `notifyListeners`; the Router asks `currentConfiguration`, hands it to the parser's `restoreRouteInformation`, and reports the resulting URL to the provider, which on the web creates a browser history entry. Both defaults return `null`, which disables reporting, so a web app must override both.

code

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

sealed class DocsPath {
  const DocsPath();
}

class HomePath extends DocsPath {
  const HomePath();
}

class ArticlePath extends DocsPath {
  const ArticlePath(this.slug);
  final String slug;
}

class DocsRouteParser extends RouteInformationParser<DocsPath> {
  @override
  Future<DocsPath> parseRouteInformation(RouteInformation routeInformation) {
    final DocsPath path = switch (routeInformation.uri.pathSegments) {
      ['docs', final String slug] => ArticlePath(slug),
      _ => const HomePath(),
    };
    return SynchronousFuture<DocsPath>(path);
  }

  @override
  RouteInformation? restoreRouteInformation(DocsPath configuration) {
    return switch (configuration) {
      HomePath() => RouteInformation(uri: Uri.parse('/')),
      ArticlePath(:final slug) => RouteInformation(uri: Uri.parse('/docs/$slug')),
    };
  }
}

class DocsRouterDelegate extends RouterDelegate<DocsPath>
    with ChangeNotifier, PopNavigatorRouterDelegateMixin<DocsPath> {
  @override
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();

  String? _slug;

  void openArticle(String slug) {
    _slug = slug;
    notifyListeners(); // Router rebuilds and reports /docs/<slug> to the browser
  }

  @override
  DocsPath get currentConfiguration {
    final String? slug = _slug;
    return slug == null ? const HomePath() : ArticlePath(slug);
  }

  @override
  Future<void> setNewRoutePath(DocsPath configuration) {
    _slug = switch (configuration) {
      HomePath() => null,
      ArticlePath(:final slug) => slug,
    };
    return SynchronousFuture<void>(null);
  }

  @override
  Widget build(BuildContext context) {
    final String? slug = _slug;
    return Navigator(
      key: navigatorKey,
      pages: [
        MaterialPage<void>(
          key: const ValueKey<String>('home'),
          child: DocsHome(onOpen: openArticle),
        ),
        if (slug != null)
          MaterialPage<void>(
            key: ValueKey<String>('doc:$slug'),
            child: ArticleScreen(slug: slug),
          ),
      ],
      onDidRemovePage: (Page<Object?> page) {
        if (page.key != const ValueKey<String>('home')) {
          _slug = null;
          notifyListeners();
        }
      },
    );
  }
}

class DocsHome extends StatelessWidget {
  const DocsHome({super.key, required this.onOpen});
  final ValueChanged<String> onOpen;

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: const Text('Docs')),
        body: ListTile(title: const Text('Layout'), onTap: () => onOpen('layout')),
      );
}

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

  @override
  Widget build(BuildContext context) => Scaffold(
        appBar: AppBar(title: Text(slug)),
        body: Center(child: Text('Article: $slug')),
      );
}

void main() {
  final DocsRouterDelegate delegate = DocsRouterDelegate();
  runApp(MaterialApp.router(
    routerDelegate: delegate,
    routeInformationParser: DocsRouteParser(),
  ));
}

go deeper

for a junior

Recall that the parser converts URLs to typed configurations and the delegate turns configurations into a Navigator with pages.

for a middle

Explain both directions, the methods called in each, and why currentConfiguration and restoreRouteInformation must be overridden for the web.

for a senior

Enforce the round-trip rule: every page-deciding state lives in the configuration, and parse and restore are exact inverses under test.

for a principal

Decide how much URL state a product should expose, balancing shareable links and history against complexity and privacy.

## The contract in one table | Direction | Step | Called on | Returns | |---|---|---|---| | URL → screen | 1 | `RouteInformationParser.parseRouteInformation(RouteInformation)` | `Future<T>` configuration | | URL → screen | 2 | `RouterDelegate.setNewRoutePath(T)` (or `setInitialRoutePath` at startup) | `Future<void>` | | URL → screen | 3 | `RouterDelegate.build(context)` | a `Navigator` with `pages` | | screen → URL | 1 | delegate state change + `notifyListeners()` | the Router rebuilds | | screen → URL | 2 | `RouterDelegate.currentConfiguration` | `T?` | | screen → URL | 3 | `RouteInformationParser.restoreRouteInformation(T)` | `RouteInformation?` | `T` is your own type, for example a sealed `DocsPath` with `HomePath` and `ArticlePath(slug)`. `RouteInformation` carries a **`uri`** (the older `location` string is deprecated) and an optional `state` object. ## Inbound: opening /docs/layout 1. On a web build, the default `PlatformRouteInformationProvider` reads the browser URL and exposes it as `RouteInformation(uri: /docs/layout)`. On mobile it comes from the initial route or a deep link. 2. The parser matches the path segments, `['docs', 'layout']`, and returns `ArticlePath('layout')`. Unknown paths should map to a not-found configuration rather than throw. 3. The Router calls `setInitialRoutePath` for the first route (by default it forwards to `setNewRoutePath`) and `setNewRoutePath` afterwards. The delegate stores `slug = 'layout'`. 4. When that future completes, the Router rebuilds; `build` returns a `Navigator` with pages `[home, article(layout)]`. Both parser and delegate methods return futures. If the work is synchronous, return a **`SynchronousFuture`**; the Router's documentation recommends it because the Router can then proceed completely synchronously, which removes a number of complications around overlapping route changes. ## Outbound: tapping "Animation" 1. A tap calls `delegate.openArticle('animation')`, which updates the state and calls `notifyListeners()`; a `RouterDelegate` is a `Listenable`, and notifications tell the Router to rebuild. 2. The Router reads `currentConfiguration`, which must describe the current state: `ArticlePath('animation')`. 3. The parser's `restoreRouteInformation` converts it to `RouteInformation(uri: /docs/animation)`. 4. The Router reports it to the provider. If the location changed, `PlatformRouteInformationProvider` asks the browser to **push** a history entry; if not, it **replaces** the current entry. `Router.navigate(context, callback)` forces a new history entry even when the URL is unchanged; `Router.neglect(context, callback)` makes a change replace the current entry instead, handy for search-as-you-type query updates. ## The round-trip rule The documentation on `currentConfiguration` is explicit: the configuration it returns must be able to reconstruct the current state if passed back to `setNewRoutePath`, otherwise the browser's back and forward buttons will not work properly. Likewise, `parseRouteInformation` must produce an equivalent configuration from what `restoreRouteInformation` returned. In practice: - every piece of state that decides which pages exist belongs in the configuration; - anything not in the configuration (an open dialog, a pageless route) is lost on back, forward or reload. ## Defaults that silently disable the web - `RouterDelegate.currentConfiguration` returns `null` by default, which prevents the Router from reporting route information. - `RouteInformationParser.restoreRouteInformation` returns `null` by default, in which case browser history is not updated and state restoration is disabled. A delegate that forgets either one still works on mobile but never updates the address bar on the web. ## Testing the pair Because the parser and delegate are plain Dart objects, most of their behaviour can be tested without a browser: - call `parseRouteInformation` with `RouteInformation(uri: Uri.parse('/docs/layout'))` and expect `ArticlePath('layout')`; - call `restoreRouteInformation` on each configuration and parse the result back, expecting the same configuration; - call `setNewRoutePath` on the delegate and check `currentConfiguration` reports the same value; - pump `MaterialApp.router` with the pair in a widget test and tap an article to confirm the pages change. ## Where the rest of the delegate comes from - `PopNavigatorRouterDelegateMixin` implements `popRoute` by calling `maybePop` on the navigator identified by the delegate's `navigatorKey`, so the Android back button pops the top page. - `ChangeNotifier` mixed into the delegate provides `addListener`, `removeListener` and `notifyListeners`. - Only one `Router` in an app should report route information, usually the top-level one created by `MaterialApp.router`.

  • Why return SynchronousFuture from parseRouteInformation and setNewRoutePath?
    The Router is designed for asynchronous parsing and route setting and waits for each future; if a new route arrives while one is pending, it has to discard or complete earlier operations. When the work is synchronous, a `SynchronousFuture` lets the Router proceed completely synchronously, which the Router documentation recommends because it removes those complications.
  • When would you use Router.neglect in the docs app?
    When a state change should update the URL without adding a history entry, for example rewriting `?q=` as the user types in the docs search box. Changes made inside the `Router.neglect` callback replace the current entry, so the back button skips the intermediate queries.

saying these in an interview costs you the question

  • Thinks the delegate parses the URL string itself
  • Expects URL updates without overriding currentConfiguration
  • Leaves restoreRouteInformation returning null on a web app
  • Changes delegate state without calling notifyListeners
  • Keeps page-deciding state outside the configuration