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?
answer
- no reports, no history entries
- two defaults that return null
- notify after every state change
- pageless pushes are invisible to the URL
- test parse and restore as inverses
basics
~20 sThe 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.
solid answer
~40 sBrowser history only contains what the Router reports, and it reports only when the delegate notifies and `currentConfiguration` plus the parser's `restoreRouteInformation` yield a `RouteInformation`. Both default to `null`, so a delegate that never overrides them works on mobile but never touches the address bar, and browser back leaves the site because there were no in-app entries. Other causes produce the same symptom: state changed without `notifyListeners`, articles opened with `Navigator.push` so they never enter the configuration, or a nested `Router` reporting instead of the top one. I would log what `currentConfiguration` returns after each tap, override both methods, derive the article page from delegate state, and add a test that `parseRouteInformation(restoreRouteInformation(c))` returns `c` for every configuration, since back and forward depend on that round trip.
code
dart · 46 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;
@override
bool operator ==(Object other) => other is ArticlePath && other.slug == slug;
@override
int get hashCode => slug.hashCode;
}
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);
}
// Without this override the default returns null and the address bar never changes.
@override
RouteInformation? restoreRouteInformation(DocsPath configuration) {
return switch (configuration) {
HomePath() => RouteInformation(uri: Uri.parse('/')),
ArticlePath(:final slug) => RouteInformation(uri: Uri.parse('/docs/$slug')),
};
}
}
Future<bool> roundTrips(DocsRouteParser parser, DocsPath config) async {
final RouteInformation? info = parser.restoreRouteInformation(config);
return info != null && await parser.parseRouteInformation(info) == config;
}go deeper
Recall that the address bar only changes when the Router reports a location, which needs currentConfiguration and restoreRouteInformation.
Explain the reporting sequence and why the two null defaults and a missing notifyListeners all produce the same symptom.
Diagnose with logging and reloads, enforce the round-trip rule with tests, and keep linkable screens out of pageless routes.
Judge when the cost of maintaining a custom delegate for web history outweighs adopting a routing package that already implements it.
## How browser history is built On the web, Flutter's `Router` keeps the browser in sync through the `PlatformRouteInformationProvider`: 1. After a rebuild triggered by the delegate's notification, the Router reads `RouterDelegate.currentConfiguration`. 2. If that is not `null`, it passes it to `RouteInformationParser.restoreRouteInformation`. 3. If that returns a `RouteInformation`, the Router reports it. A changed location pushes a new browser history entry; an unchanged one replaces the current entry. 4. When the user presses the browser's back or forward button, the browser delivers the stored location back through the provider, the parser parses it, and the delegate's `setNewRoutePath` applies it. If step 1, 2 or 3 never produces a report, the browser has only the entry for the page the site was loaded from. The back button then does what it does on any page with one entry: it leaves the site. ## Causes, most likely first | Cause | Why nothing is reported | Fix | |---|---|---| | `currentConfiguration` not overridden | default returns `null`, which prevents reporting | return a configuration built from delegate state | | `restoreRouteInformation` not overridden | default returns `null`; history is not updated | map every configuration to a `RouteInformation(uri: ...)` | | state changed without `notifyListeners` | the Router never rebuilds, so it never asks | notify after every navigation state change | | articles opened with `Navigator.push` | pageless routes are not in the configuration | open articles by changing delegate state | | a nested `Router` given the provider and parser | the wrong router reports, or both fight | only the top-level router handles route information | ## A second symptom: back works but shows the wrong page Sometimes history entries exist but back and forward land on the wrong screen. The Router's documentation states the rule that governs this: the configuration returned by `currentConfiguration` must be able to reconstruct the current state when passed back to `setNewRoutePath`, or the browser buttons will not work properly. Typical violations: - `currentConfiguration` returns only the article slug, but the page stack also depends on an open section or version selector kept elsewhere; - `setNewRoutePath` ignores part of the configuration, for example it opens the article but does not clear a previously open search results page; - `parseRouteInformation` and `restoreRouteInformation` are not inverses, e.g. restore writes `/docs/layout/` and parse only recognizes `/docs/layout`. ## Debugging steps 1. Log inside `currentConfiguration` and `restoreRouteInformation`. If they are never called after a tap, the delegate is not notifying. 2. If they return `null`, you have found the default. 3. Check how articles are opened. A `Navigator.push` call means the article is pageless and cannot appear in the URL. 4. Reload the page on `/docs/layout`. If the article does not open, the inbound path (`parseRouteInformation` → `setNewRoutePath`) is broken, which will also break back and forward. 5. Remember the browser's model: back is **reverse chronological**. Popping the article in-app and then pressing browser back brings the article back, which is correct behaviour, not a bug. ## The fix, sketched ```dart @override DocsPath get currentConfiguration { final String? slug = _slug; return slug == null ? const HomePath() : ArticlePath(slug); } @override RouteInformation? restoreRouteInformation(DocsPath configuration) { return switch (configuration) { HomePath() => RouteInformation(uri: Uri.parse('/')), ArticlePath(:final slug) => RouteInformation(uri: Uri.parse('/docs/$slug')), }; } ``` and every navigation method on the delegate ends with `notifyListeners()`. ## Preventing regressions - A unit test that, for each configuration, `parseRouteInformation(restoreRouteInformation(c))` equals `c`. - A widget test that taps an article and asserts the delegate's `currentConfiguration`, which is what the URL is built from. - A rule in review: screens that must be linkable are pages in the delegate's state; `Navigator.push` is reserved for dialogs and result-returning pickers. - If a routing package fits, go_router implements this reporting for you; many teams move there after debugging the first custom delegate.
- The user pops an article with the in-app back arrow, then presses the browser's back button, and the article reappears. Is that a bug?No. Browser back is reverse chronological: it returns to the previous location the Router reported, which was the article. The in-app pop reported home as a new entry, so going back from home goes to the article again. If that is undesirable for a particular action, make that change inside `Router.neglect` so it replaces the entry instead.
- Why can't an article opened with Navigator.push be restored after a page reload?A pushed route is pageless: it is not in the delegate's state, so `currentConfiguration` never describes it and no URL is reported for it. After a reload, only the reported URL exists, the parser produces the configuration without the article, and the pageless route is gone.
saying these in an interview costs you the question
- Blames the browser or the URL strategy for missing history
- Assumes the Router infers the URL from the top page
- Fixes it by calling a browser history API from Dart directly
- Treats reverse-chronological browser back as a Router bug
- Opens linkable screens with Navigator.push in a Router app