Skip to content

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

Closed
flutter/packages
#9436

Description

@jonmountjoy

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

Activity

  1. danagbemava-nc commented on Sep 23, 2022

    @danagbemava-nc
    Member

    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!
    
  2. added
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    and removed
    in triagePresently being triaged by the triage team
    on Sep 23, 2022
  3. added
    P2Important issues not at the top of the work list
    on Sep 27, 2022
  4. chunhtai commented on Sep 29, 2022

    @chunhtai
    Contributor

    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.

  5. ryanheise commented on Sep 30, 2022

    @ryanheise

    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.

  6. jonmountjoy commented on Sep 30, 2022

    @jonmountjoy
    ContributorAuthor

    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.

  7. ab36245 commented on Oct 11, 2022

    @ab36245

    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 GoNavigatorObserver as 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.

  8. 59 remaining items

  9. mhkarami commented on Jul 2, 2024

    @mhkarami

    i use redirect instead :

    final go_router = GoRouter(
      redirect: routeGuard,
    ...
    
    var routeGuard = (BuildContext context, GoRouterState state) async {
      var destination = state.uri.path;
      Log.i("destination : $destination");
      if(destination != null && destination.isNotEmpty){
      ... send to analytics
      }
    
  10. kmrosiek commented on Jul 25, 2024

    @kmrosiek

    i use redirect instead :

    final go_router = GoRouter(
      redirect: routeGuard,
    ...
    
    var routeGuard = (BuildContext context, GoRouterState state) async {
      var destination = state.uri.path;
      Log.i("destination : $destination");
      if(destination != null && destination.isNotEmpty){
      ... send to analytics
      }
    

    How do you detect pop when using redirect?

  11. ryanovas commented on Feb 14, 2025

    @ryanovas

    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:

    GoRoute(
      name: 'page',
      path: '/page/:slug',
      builder: (context, state) {
        final slug = state.pathParameters['slug']!.toLowerCase();
        return Page(
          key: UniqueKey(),
          slug: slug,
        );
      },
    )

    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.

  12. JarvanMo commented on Jun 24, 2025

    @JarvanMo
  13. added
    r: fixedIssue is closed as already fixed in a newer version
    on Nov 7, 2025
  14. github-actions commented on Nov 21, 2025

    @github-actions

    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 -v and a minimal reproduction of the issue.

  15. locked as resolved and limited conversation to collaborators on Nov 21, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Important issues not at the top of the work listfound in release: 3.3Found to occur in 3.3found in release: 3.4Found to occur in 3.4has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.r: fixedIssue is closed as already fixed in a newer versionteam-frameworkOwned by Framework teamtriaged-frameworkTriaged by Framework team

    Type

    No type

    Projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions