Repository navigation
[go_router] ShellRoutes seem to cause NavigatorObserver to not fire (5.0.1) #112196
Description
Steps to Reproduce
See the code below, or this repo.
Expected results:
I would expect didPush in NavigatorObserver to be called for every navigation to any page.
Actual results:
It doesn't get called at all, except for the page b/details possibly because it's using a _rootNavigatorKey?
So in the app, if you click on each tab, and each detail page, (and back again), you only see two logs:
[MyNavObserver] didPush: route(/b/details: {}), previousRoute= route(: {})
[MyNavObserver] didPop: route(/b/details: {}), previousRoute= route(: {})
Code sample
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
final GlobalKey<NavigatorState> _rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'shell');
// This scenario demonstrates how to set up nested navigation using ShellRoute,
// which is a pattern where an additional Navigator is placed in the widget tree
// to be used instead of the root navigator. This allows deep-links to display
// pages along with other UI components such as a BottomNavigationBar.
//
// This example demonstrates how to display a route within a ShellRoute and also
// push a screen using a different navigator (such as the root Navigator) by
// providing a `parentNavigatorKey`.
void main() {
runApp(ShellRouteExampleApp());
}
/// An example demonstrating how to use [ShellRoute]
class ShellRouteExampleApp extends StatelessWidget {
/// Creates a [ShellRouteExampleApp]
ShellRouteExampleApp({Key? key}) : super(key: key);
final GoRouter _router = GoRouter(
observers: <NavigatorObserver>[MyNavObserver()],
debugLogDiagnostics: true,
navigatorKey: _rootNavigatorKey,
initialLocation: '/a',
routes: <RouteBase>[
/// Application shell
ShellRoute(
navigatorKey: _shellNavigatorKey,
builder: (BuildContext context, GoRouterState state, Widget child) {
return ScaffoldWithNavBar(child: child);
},
routes: <RouteBase>[
/// The first screen to display in the bottom navigation bar.
GoRoute(
path: '/a',
builder: (BuildContext context, GoRouterState state) {
return const ScreenA();
},
routes: <RouteBase>[
// The details screen to display stacked on the inner Navigator.
// This will cover screen A but not the application shell.
GoRoute(
path: 'details',
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'A');
},
),
],
),
/// Displayed when the second item in the the bottom navigation bar is
/// selected.
GoRoute(
path: '/b',
builder: (BuildContext context, GoRouterState state) {
return const ScreenB();
},
routes: <RouteBase>[
/// Same as "/a/details", but displayed on the root Navigator by
/// specifying [parentNavigatorKey]. This will cover both screen B
/// and the application shell.
GoRoute(
path: 'details',
parentNavigatorKey: _rootNavigatorKey,
builder: (BuildContext context, GoRouterState state) {
return const DetailsScreen(label: 'B');
},
),
],
),
],
),
],
);
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'Flutter Defmo',
theme: ThemeData(
primarySwatch: Colors.red,
),
routeInformationParser: _router.routeInformationParser,
routerDelegate: _router.routerDelegate,
routeInformationProvider: _router.routeInformationProvider,
);
}
}
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
class ScaffoldWithNavBar extends StatelessWidget {
/// Constructs an [ScaffoldWithNavBar].
const ScaffoldWithNavBar({
required this.child,
Key? key,
}) : super(key: key);
/// The widget to display in the body of the Scaffold.
/// In this sample, it is a Navigator.
final Widget child;
@override
Widget build(BuildContext context) {
return Scaffold(
body: child,
bottomNavigationBar: BottomNavigationBar(
items: const <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: Icon(Icons.home),
label: 'A Screen',
),
BottomNavigationBarItem(
icon: Icon(Icons.business),
label: 'B Screen',
),
],
currentIndex: _calculateSelectedIndex(context),
onTap: (int idx) => _onItemTapped(idx, context),
),
);
}
static int _calculateSelectedIndex(BuildContext context) {
final GoRouter route = GoRouter.of(context);
final String location = route.location;
if (location == '/a') {
return 0;
}
if (location == '/b') {
return 1;
}
return 0;
}
void _onItemTapped(int index, BuildContext context) {
switch (index) {
case 0:
GoRouter.of(context).go('/a');
break;
case 1:
GoRouter.of(context).go('/b');
break;
}
}
}
/// The first screen in the bottom navigation bar.
class ScreenA extends StatelessWidget {
/// Constructs a [ScreenA] widget.
const ScreenA({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Screen A'),
TextButton(
onPressed: () {
GoRouter.of(context).go('/a/details');
},
child: const Text('View A details'),
),
],
),
),
);
}
}
/// The second screen in the bottom navigation bar.
class ScreenB extends StatelessWidget {
/// Constructs a [ScreenB] widget.
const ScreenB({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
const Text('Screen B'),
TextButton(
onPressed: () {
GoRouter.of(context).go('/b/details');
},
child: const Text('View B details'),
),
],
),
),
);
}
}
/// The details screen for either the A or B screen.
class DetailsScreen extends StatelessWidget {
/// Constructs a [DetailsScreen].
const DetailsScreen({
required this.label,
Key? key,
}) : super(key: key);
/// The label to display in the center of the screen.
final String label;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Details Screen'),
),
body: Center(
child: Text(
'Details for $label',
style: Theme.of(context).textTheme.headlineMedium,
),
),
);
}
}
/// The Navigator observer.
class MyNavObserver extends NavigatorObserver {
/// Creates a [MyNavObserver].
MyNavObserver() {
log.onRecord.listen((LogRecord e) => debugPrint('$e'));
}
/// The logged message.
final Logger log = Logger('MyNavObserver');
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
log.info('didPush: ${route.str}, previousRoute= ${previousRoute?.str}');
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) =>
log.info('didPop: ${route.str}, previousRoute= ${previousRoute?.str}');
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) =>
log.info('didRemove: ${route.str}, previousRoute= ${previousRoute?.str}');
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) =>
log.info('didReplace: new= ${newRoute?.str}, old= ${oldRoute?.str}');
}
extension on Route<dynamic> {
String get str => 'route(${settings.name}: ${settings.arguments})';
}
flutter doctor -v
[✓] Flutter (Channel stable, 3.3.2, on macOS 12.6 21G115 darwin-x64, locale en-GB) • Flutter version 3.3.2 on channel stable at /Users/jonmountjoy/flutter • Upstream repository https://github.com/flutter/flutter.git • Framework revision e3c29ec (8 days ago), 2022-09-14 08:46:55 -0500 • Engine revision a4ff2c53d8 • Dart version 2.18.1 • DevTools version 2.15.0[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.3)
• Android SDK at /Users/jonmountjoy/Library/Android/sdk
• Platform android-33, build-tools 30.0.3
• ANDROID_HOME = /Users/jonmountjoy/Library/Android/sdk
• Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 14.0)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Build 14A309
• CocoaPods version 1.11.3
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
danagbemava-nc commentedon Sep 23, 2022
I can reproduce the issue using the code sample provided above.
logs
flutter doctor -v
chunhtai commentedon Sep 29, 2022
Navigator observer is not supposed to be used on multiple navigator, we need to figure out whether or how to share a observer on multiple navigator or make the shell route to take a list of observer, too.
ryanheise commentedon Sep 30, 2022
I think it's a bit broader than NavigatorObserver,
router.addListener()will also not fire. We have a situation where the global state ofrouter.locationwill be updated to reflect the current global route, but the global listenerrouter.addListener()will not fire whenever that location changes. If you use a bottom nav bar to drive a shell router at the top of your stack, then basicallyrouter.addListener()will fire only once when the app loads, despiterouter.locationchanging every time you navigate to the sub-routes. The closest listener we have currently to that is theredirectcallback, although it wasn't designed for that purpose.GoRouter is designed to abstract away the nested navigators, routes and sub-routes into a global navigation structure that maps to a single, global URI path. Globally, each route path corresponds to a stack of pages. So rather than be able to listen to individual nested navigators individually, I think an ideal API would allow you to listen to whenever the current "global" route changed, and give you access to this complete stack of pages on each page navigation. It could also be helpful to have a counterpart to
NavigatorObservercalledGoNavigatorObserverthat has all the same methods as the original, but with the parameters specialised to go_router state, and giving you the full page stack of state as the parameters.If people would like to still observe multiple individual navigators, I would not object to having that API as well, although at a minimum, since go_router is designed to abstract the navigator state into transitions between page stacks, we should have an API to listen to those state changes at go_router's notion of abstraction.
jonmountjoy commentedon Sep 30, 2022
Thank you for the clear explanation @ryanheise . So there's no easy way to use Firebase Analytics with go_router. I guess I could wrap each and every
GoRouter.of(context).pushin my own method which first calls analytics 😢 I suspect the redirect callback might also work - despite being ugly - it may be a quick temporary fix. But like many apps, I don't always use GoRouter. I sometimes use good old Navigator. and the mix of the two analytics is going to be unpleasant.Your ideal API sounds ideal, and is more expansive than my simple needs. All I really need is the current
router.location, and ideally the previous - which is enough for my analytics needs. I can see how the page stack and parameters could enable a lot more of course.ab36245 commentedon Oct 11, 2022
I am also struggling with wanting to know the current state of the routes. The problem is exactly as descibed here - the observer passed to GoRouter is only fired at startup. It makes sense why, as ShellRoute creates a nested Navigator, but it's not possible to add an observer to this nested Navigator.
@ryanheise I think
GoNavigatorObserveras you describe it would be ideal. I would suggest that you could also add observers to ShellRoute as you mentioned as this gives the most flexibility.59 remaining items
mhkarami commentedon Jul 2, 2024
i use redirect instead :
kmrosiek commentedon Jul 25, 2024
How do you detect
popwhen usingredirect?ryanovas commentedon Feb 14, 2025
I'm not sure if this is related, but I'm having a similar issue using NavigatorObserver where if you have a route with path parameters like so:
Changing the slug when you're on the page already doesn't trigger any push or replace events or anything. Also, on web, if you don't add the UniqueKey() part, the browser back and forward buttons won't trigger rebuilds to show the correct UI.
JarvanMo commentedon Jun 24, 2025
[go_router] ShellRoute will merge GoRouter's observers (flutter#9436)
github-actions commentedon Nov 21, 2025
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.[go_router] ShellRoute will merge GoRouter's observers (flutter#9436)