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?
answer
- URL to typed config, config to URL
- parseRouteInformation then setNewRoutePath
- notifyListeners makes the Router rebuild
- currentConfiguration and restoreRouteInformation
- SynchronousFuture when no async work
basics
~20 sOn 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 sThe 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 linesimport '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
Recall that the parser converts URLs to typed configurations and the delegate turns configurations into a Navigator with pages.
Explain both directions, the methods called in each, and why currentConfiguration and restoreRouteInformation must be overridden for the web.
Enforce the round-trip rule: every page-deciding state lives in the configuration, and parse and restore are exact inverses under test.
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