Repository navigation
[go_router] onExit fires spuriously for a pushed route when refreshListenable fires #189578
Description
What package does this bug report belong to?
go_router
What target platforms are you seeing this bug on?
Android, iOS, Web, macOS, Linux, Windows
Have you already upgraded your packages?
Yes
Dependency versions
pubspec.lock
go_router 17.3.0
(also reproduced against packages/go_router at flutter/packages main, 9e58c600b0)
Steps to reproduce
- Run the code sample below.
- Tap "Push form" (
context.pushof a route that definesonExit). - Tap "Trigger refreshListenable" (stands in for any real-world tick: an auth refresh, a tab-refocus listener, etc.).
Expected results
Nothing happens — the pushed route never left the stack, so onExit must not run.
Actual results
The console prints onExit called on every refresh tick. For routes that use onExit to guard unsaved changes, the user gets a spurious "discard changes?" prompt every time refreshListenable fires while the route is on top.
Cause: ImperativeRouteMatch includes its push completer in operator==/hashCode, but the completer can't survive a codec round-trip — a refreshListenable re-parse (and state restoration) decodes a fresh Completer, so the re-decoded match list compares unequal to the live one even though nothing changed. That defeats setNewRoutePath's no-op fast path, and the router treats the re-parse as a real navigation change, running onExit against a route that is still on the stack.
Fix (see the linked PR): keep the completer in equality and instead have the parser carry over the live completer for each imperative match by pageKey (unique per push) when it re-decodes the current configuration (state restoration, browser history, refreshListenable re-parses). The re-decoded list then compares equal to the live one — completer included — so the no-op fast path holds and onExit doesn't fire; in-flight push futures are preserved, and imperative push/replace/pushReplacement are untouched. (Simply excluding the completer from equality is not a safe fix: same-location replace() deliberately reuses pageKey and differs only by its new completer, so its returned future would be orphaned.)
Code sample
Code sample
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
final ValueNotifier<int> refresh = ValueNotifier<int>(0);
void main() => runApp(App());
class App extends StatelessWidget {
App({super.key});
late final GoRouter router = GoRouter(
refreshListenable: refresh,
routes: [
GoRoute(
path: '/',
builder: (_, __) => Scaffold(
body: Center(
child: Builder(
builder: (context) => TextButton(
onPressed: () => context.push('/form'),
child: const Text('Push form'),
),
),
),
),
),
GoRoute(
path: '/form',
onExit: (_, __) {
debugPrint('onExit called');
return true;
},
builder: (_, __) => Scaffold(
body: Center(
child: TextButton(
onPressed: () => refresh.value++,
child: const Text('Trigger refreshListenable'),
),
),
),
),
],
);
@override
Widget build(BuildContext context) => MaterialApp.router(routerConfig: router);
}Screenshots or Videos
Screenshots / Video demonstration
N/A (behavior visible in console output).
Logs
Logs
flutter: onExit called(printed on each "Trigger refreshListenable" tap, although no navigation happened)
Flutter Doctor output
Doctor output
[✓] Flutter (Channel stable, 3.44.4, on macOS 26.5.2 25F84 darwin-arm64, locale en-US) [609ms]
• Flutter version 3.44.4 on channel stable at ~/repos/flutter
• Upstream repository https://github.com/flutter/flutter.git
• Framework revision ad70ec4617 (3 weeks ago), 2026-06-24 11:07:06 -0700
• Engine revision a10d8ac38d
• Dart version 3.12.2
• DevTools version 2.57.0
• Feature flags: enable-web, enable-linux-desktop, enable-macos-desktop, enable-windows-desktop, enable-android, enable-ios, cli-animations, enable-native-assets, enable-swift-package-manager, omit-legacy-version-file, enable-lldb-debugging, enable-uiscene-migration
[✓] Android toolchain - develop for Android devices (Android SDK version 36.0.0) [1,824ms]
• Android SDK at ~/Library/Android/sdk
• Emulator version unknown
• Platform android-36, build-tools 36.0.0
• ANDROID_HOME = ~/Library/Android/sdk
• Java binary at: ~/Applications/Android Studio.app/Contents/jbr/Contents/Home/bin/java
This is the JDK bundled with the latest Android Studio installation on this machine.
To manually set the JDK path, use: `flutter config --jdk-dir="path/to/jdk"`.
• Java version OpenJDK Runtime Environment (build 21.0.6+-13391695-b895.109)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 26.6) [1,335ms]
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 17F113
• CocoaPods version 1.16.2
[✓] Chrome - develop for the web [6ms]
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Connected device (2 available) [6.8s]
• macOS (desktop) • macos • darwin-arm64 • macOS 26.5.2 25F84 darwin-arm64
• Chrome (web) • chrome • web-javascript • Google Chrome 150.0.7871.116
[✓] Network resources [789ms]
• All expected network resources are available.
• No issues found!
Activity
mbcorona commentedon Jul 16, 2026
Thanks for the report and linked PR.
Routing to the framework team; PR flutter/packages#12212 is linked.