[go_router] ShellRoutes seem to cause NavigatorObserver to not fire (5.0.1)

This issue has been created since 2022-09-22.

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

danagbemava-nc wrote this answer on 2022-09-23

I can reproduce the issue using the code sample provided above.

logs
Launching lib/main.dart on macOS in debug mode...
--- xcodebuild: WARNING: Using the first of multiple matching destinations:
{ platform:macOS, arch:arm64, id:00008103-000329201AD1001E }
{ platform:macOS, arch:x86_64, id:00008103-000329201AD1001E }
Connecting to VM Service at ws://127.0.0.1:57184/EJ7oTZCplKM=/ws
flutter: [INFO] GoRouter: Full paths for routes:
flutter:
[GoRouter] Full paths for routes:
flutter: [INFO] GoRouter: setting initial location /a
[GoRouter] setting initial location /a
flutter: [INFO] GoRouter: Using MaterialApp configuration
[GoRouter] Using MaterialApp configuration
flutter: [INFO] MyNavObserver: didPush: route(: {}), previousRoute= null
[MyNavObserver] didPush: route(: {}), previousRoute= null
flutter: [INFO] GoRouter: going to /a/details
[GoRouter] going to /a/details
flutter: [INFO] GoRouter: going to /b
[GoRouter] going to /b
flutter: [INFO] GoRouter: going to /a
[GoRouter] going to /a
flutter: [INFO] GoRouter: going to /b
[GoRouter] going to /b
flutter: [INFO] GoRouter: going to /b/details
[GoRouter] going to /b/details
flutter: [INFO] MyNavObserver: didPush: route(/b/details: {}), previousRoute= route(: {})
[MyNavObserver] didPush: route(/b/details: {}), previousRoute= route(: {})
Application finished.
Exited
flutter doctor -v
[✓] Flutter (Channel stable, 3.3.2, on macOS 12.6 21G115 darwin-arm, locale en-GB)
    • Flutter version 3.3.2 on channel stable at /Users/nexus/dev/sdks/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision e3c29ec00c (9 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 33.0.0)
    • Android SDK at /Users/nexus/Library/Android/sdk
    • Platform android-33, build-tools 33.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • 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

[✓] Android Studio (version 2021.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[✓] Android Studio (version 2021.2)
    • Android Studio at /Users/nexus/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/212.5712.43.2112.8815526/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.71.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.48.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-arm64   • macOS 12.6 21G115 darwin-arm
    • Chrome (web)    • chrome • web-javascript • Google Chrome 105.0.5195.125

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
[✓] Flutter (Channel master, 3.4.0-28.0.pre.123, on macOS 12.6 21G115 darwin-arm64, locale en-GB)
    • Flutter version 3.4.0-28.0.pre.123 on channel master at /Users/nexus/dev/sdks/flutters
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 4aa27d844e (4 hours ago), 2022-09-22 21:00:22 -0700
    • Engine revision a5489ce4f9
    • Dart version 2.19.0 (build 2.19.0-229.0.dev)
    • DevTools version 2.17.0

[✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0)
    • Android SDK at /Users/nexus/Library/Android/sdk
    • Platform android-33, build-tools 33.0.0
    • Java binary at: /Applications/Android Studio.app/Contents/jre/Contents/Home/bin/java
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)
    • 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

[✓] Android Studio (version 2021.3)
    • Android Studio at /Applications/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.13+0-b1751.21-8125866)

[✓] Android Studio (version 2021.2)
    • Android Studio at /Users/nexus/Library/Application Support/JetBrains/Toolbox/apps/AndroidStudio/ch-0/212.5712.43.2112.8815526/Android Studio.app/Contents
    • Flutter plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/9212-flutter
    • Dart plugin can be installed from:
      🔨 https://plugins.jetbrains.com/plugin/6351-dart
    • Java version OpenJDK Runtime Environment (build 11.0.12+0-b1504.28-7817840)

[✓] VS Code (version 1.71.2)
    • VS Code at /Applications/Visual Studio Code.app/Contents
    • Flutter extension version 3.48.0

[✓] Connected device (2 available)
    • macOS (desktop) • macos  • darwin-arm64   • macOS 12.6 21G115 darwin-arm64
    • Chrome (web)    • chrome • web-javascript • Google Chrome 105.0.5195.125

[✓] HTTP Host Availability
    • All required HTTP hosts are available

• No issues found!
chunhtai wrote this answer on 2022-09-29

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 wrote this answer on 2022-09-30

I think it's a bit broader than NavigatorObserver, router.addListener() will also not fire. We have a situation where the global state of router.location will be updated to reflect the current global route, but the global listener router.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 basically router.addListener() will fire only once when the app loads, despite router.location changing every time you navigate to the sub-routes. The closest listener we have currently to that is the redirect callback, 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 NavigatorObserver called GoNavigatorObserver that 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 wrote this answer on 2022-09-30

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).push in 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.

More Details About Repo
Owner Name flutter
Repo Name flutter
Full Name flutter/flutter
Language Dart
Created Date 2015-03-06
Updated Date 2022-10-05
Star Count 145512
Watcher Count 3569
Fork Count 23398
Issue Count 11205

YOU MAY BE INTERESTED

Issue Title Created Date Comment Count Updated Date
Android- programmatically specify API key? 4 2018-08-14 2022-05-08
Get viewport bounds? 3 2018-08-08 2022-05-08
Option to create folders 0 2021-02-08 2022-10-05
AwsHttpGateway does not support api_gateway_base_path 0 2021-10-05 2022-07-09
Mangum 0.12 regression: Unable to determine handler from trigger event 5 2021-07-19 2022-05-23
Raise an exception when `RomanNumeral` gets non-numeric non-accidental figures 1 2022-04-26 2022-09-11
Catching Errors - Event Handler 6 2021-12-01 2022-10-02
Limitation of the "when" function to 5 elements 2 2021-09-23 2022-09-20
minZoom Prop is not working as expected 0 2022-05-12 2022-09-03
React 18 typescript problem 9 2022-04-12 2022-10-03
Calendar view doesn't show Monday as first day of week 1 2022-06-15 2022-09-16
hanime.tv is incompatible with 2 2022-05-04 2022-06-30
Use locale-independent double parser 1 2021-12-05 2022-09-19
3.0: libsemanage uses libsepol non-public symbols. 18 2020-02-22 2022-09-09
At the bottom of readme, API Reference link to 404 1 2022-05-20 2022-09-27
文档有误:服务端->实时消息服务->监听单个消息,call: MsgChat 类型应为MsgCall<MsgChat> 2 2022-02-14 2022-09-11
项目打包时报错:no such file or directory, open 'dist/package.json' 1 2022-09-06 2022-10-03
Condition with is set or is not set doesn't work with CoreWorkflow 3 2021-10-19 2022-08-30
[dynmap] Cannot find [Lnet.minecraft.server.BiomeBase; 1 2021-06-30 2022-10-02
Error generating android package 3 2021-09-13 2022-08-07
Cilium agent stopped working 4 2021-06-18 2022-09-18
Mask doesn’t move with interactive viewport 2 2020-12-01 2022-02-18
Steam Audio Binaural / Unity - audio crackling when blending between binaural and 2D stereo 0 2021-02-10 2022-09-24
Issue when importing the Connect Flow in non-US-East locations 2 2021-02-16 2022-10-02
We need to not cross the node/Electron border other than a preload file 0 2022-06-05 2022-09-22
Address Class only allows numbers for ZIP code by default 4 2022-02-07 2022-09-26
Environment Variables different in DrRacket 2 2022-04-22 2022-10-03
[whiteout] demo fails on freebsd 5 2021-11-22 2022-07-29
notcurses-tester hangs on windows 1 2021-11-22 2021-12-06
Basic log docs missing details from PG blogs 1 2022-09-10 2022-09-15
Update definitions of `biogasoline` and `biodiesel` 10 2021-11-26 2022-07-12
Difference in the data between the CellProfiler data and the Efficientnet data 6 2021-10-21 2022-08-30
Investigate moving some extension scripts to the end of <body> 6 2021-08-25 2022-06-28
Web cache poisoning -- `X-Forwarded-Scheme` vs `X-Forwarded-Proto` 8 2022-02-15 2022-09-29
Add presets for buoys other than red and green 5 2022-06-23 2022-09-15
[Enhancement] Add a way to know if FxA Prod/Stage is used while In App Authentication flow 0 2022-05-04 2022-08-29
Path incorrectly decoded during clean 12 2018-03-13 2022-08-15
Adding toggle switch for Light/Dark mode 2 2022-02-04 2022-10-05
[stable/nfs-server-provisioner] POD can't mount existing PV 4 2020-12-29 2022-10-03
Restructure MinIO Baremetal Docs 0 2020-12-01 2022-09-11
Getting duplicate value from `StringSliceVarP` 0 2022-04-30 2022-10-02
Incorrect map path on kaggle dataset 6 2019-09-13 2022-10-03
Unable to invoke lambda when "Authorization scopes" added to JWT Authorizer 2 2021-04-26 2022-09-21
Leak on parceler compiler (org.parceler.ParcelAnnotationProcessor$$Bootstrap) 31 2020-01-06 2022-09-27
Python推理时出错Exception: transforms need to be defined, now is None. 2 2022-03-01 2022-09-11
LSP crashes when using mixins and the `on` clause 2 2022-05-31 2022-09-25
HA NFC tags documentation 1 2022-08-19 2022-10-03
ci: our test suite is currently broken and needs to be fixed 0 2022-09-09 2022-09-29
Add spec comparing licenses in bintry to the licenses in Bintray 1 2019-06-11 2022-09-08
Align punctuation in Exception messages with Spring Data Commons 0 2022-06-13 2022-09-25