Skip to content

[go_router] Android predictive back pops one screen from each StatefulShellBranch and popped screens dispose called in the wrong order #188018

Open
flutter/packages
#11910

Description

@jasaw

Steps to reproduce

  1. Make sure predictive back is enabled (either Android 16 SDK 36 or set android:enableOnBackInvokedCallback="true" in main/AndroidManifest.xml).
  2. Run the sample code below on Android 16.
  3. On the initial "Stack A" screen, push the button Push one screen twice to push 2 screens into branch A.
  4. Use the bottom navigation bar to navigate to C. Push the button once to push 1 screen into branch C.
  5. Use the bottom navigation bar to navigate to B.
  6. Do the predictive back gesture twice (swipe from left edge of the screen to the middle).
  7. Use the bottom navigation bar to navigate to A.

Expected results

Screens from inactive branch A and C should not pop off.

Actual results

Each predictive back pops one screen off each of the 3 branches if there is something to pop:

  1. First predictive back pops screen A3 and C2.
  2. Second predictive back pops screen A2.

It does not call the screen dispose function after the screens are popped off. If we navigate to branch A, the dispose function for both screen A2 and A3 are called but in the wrong order. When popping off screens normally, A3 dispose should be called first, then follow by A2. In this case, it's A2 first, then A3.

Code sample

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';

void main() {
  runApp(const MyApp());
}

final GoRouter _router = GoRouter(
  initialLocation: '/A1',
  routes: [
    StatefulShellRoute.indexedStack(
      builder:
          (
            BuildContext context,
            GoRouterState state,
            StatefulNavigationShell navigationShell,
          ) {
            return BottomNavWrapper(navigationShell: navigationShell);
          },
      branches: <StatefulShellBranch>[
        StatefulShellBranch(
          initialLocation: '/A1',
          observers: [LoggingObserver('/A')],
          routes: <RouteBase>[
            GoRoute(
              path: '/A1',
              builder: (BuildContext context, GoRouterState state) =>
                  MyTestPage(
                    title: 'Stack A - 1',
                    nextRoutePath: '/A1/A2',
                    canPop: false,
                  ),
              routes: [
                GoRoute(
                  path: '/A2',
                  builder: (BuildContext context, GoRouterState state) =>
                      MyTestPage(
                        title: 'Stack A - 2',
                        nextRoutePath: '/A1/A2/A3',
                      ),
                  routes: [
                    GoRoute(
                      path: '/A3',
                      builder: (BuildContext context, GoRouterState state) =>
                          MyTestPage(title: 'Stack A - 3'),
                    ),
                  ],
                ),
              ],
            ),
          ],
        ),

        StatefulShellBranch(
          initialLocation: '/B1',
          observers: [LoggingObserver('/B')],
          routes: <RouteBase>[
            GoRoute(
              path: '/B1',
              builder: (BuildContext context, GoRouterState state) =>
                  MyTestPage(title: 'Stack B - 1', canPop: false),
            ),
          ],
        ),

        StatefulShellBranch(
          initialLocation: '/C1',
          observers: [LoggingObserver('/C')],
          routes: <RouteBase>[
            GoRoute(
              path: '/C1',
              builder: (BuildContext context, GoRouterState state) =>
                  MyTestPage(
                    title: 'Stack C - 1',
                    nextRoutePath: '/C1/C2',
                    canPop: false,
                  ),
              routes: [
                GoRoute(
                  path: '/C2',
                  builder: (BuildContext context, GoRouterState state) =>
                      MyTestPage(title: 'Stack C - 2'),
                ),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
);

class BottomNavWrapper extends StatelessWidget {
  final StatefulNavigationShell navigationShell;

  const BottomNavWrapper({super.key, required this.navigationShell});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: navigationShell,
      bottomNavigationBar: NavigationBar(
        onDestinationSelected: (int index) {
          navigationShell.goBranch(index, initialLocation: false);
        },
        selectedIndex: navigationShell.currentIndex,
        destinations: <Widget>[
          NavigationDestination(
            icon: Icon(Icons.navigation_outlined),
            selectedIcon: Icon(Icons.navigation),
            label: 'A',
          ),
          NavigationDestination(
            icon: Icon(Icons.navigation_outlined),
            selectedIcon: Icon(Icons.navigation),
            label: 'B',
          ),
          NavigationDestination(
            icon: Icon(Icons.navigation_outlined),
            selectedIcon: Icon(Icons.navigation),
            label: 'C',
          ),
        ],
      ),
    );
  }
}

class LoggingObserver extends NavigatorObserver {
  final String name;

  LoggingObserver(this.name);

  @override
  void didPop(Route route, Route? previousRoute) {
    debugPrint(
      '$name didPop ${route.settings.name} -> ${previousRoute?.settings.name}',
    );
  }

  @override
  void didPush(Route route, Route? previousRoute) {
    debugPrint('$name didPush ${route.settings.name}');
  }
}

class MyApp extends StatefulWidget {
  const MyApp({super.key});

  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    _router.routerDelegate.addListener(() {
      debugPrint(
        'Location: '
        '${_router.routerDelegate.currentConfiguration.last.matchedLocation}',
      );
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp.router(title: 'Go Router Bug', routerConfig: _router);
  }
}

class MyTestPage extends StatefulWidget {
  const MyTestPage({
    super.key,
    required this.title,
    this.nextRoutePath,
    this.canPop = true,
  });

  final String title;
  final String? nextRoutePath;
  final bool canPop;

  @override
  State<MyTestPage> createState() => _MyTestPageState();
}

class _MyTestPageState extends State<MyTestPage> {
  @override
  void dispose() {
    debugPrint('${widget.title} dispose');
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return PopScope(
      canPop: widget.canPop,
      onPopInvokedWithResult: (bool didPop, Object? result) {
        debugPrint('${widget.title} onPopInvokedWithResult didPop=$didPop');
      },
      child: Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
          leading: widget.canPop
              ? IconButton(
                  icon: const Icon(Icons.chevron_left),
                  onPressed: () => Navigator.of(context).maybePop(),
                )
              : null,
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: .center,
            children: [
              Text(
                widget.title,
                style: Theme.of(context).textTheme.headlineMedium,
              ),
              if (widget.nextRoutePath != null)
                Padding(
                  padding: const EdgeInsets.only(top: 16.0),
                  child: ElevatedButton(
                    onPressed: () => context.push(widget.nextRoutePath!),
                    child: Text('Push one screen'),
                  ),
                ),
            ],
          ),
        ),
      ),
    );
  }
}

Screenshots or Video

Screenshots / Video demonstration
Screen_recording_20260615_233149.webm

Logs

Logs
I/flutter (11158): Location: /A1
I/flutter (11158): /A didPush /A1
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Location: /A1/A2
I/flutter (11158): /A didPush /A2
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Location: /A1/A2/A3
I/flutter (11158): /A didPush /A3
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Location: /C1
I/flutter (11158): /C didPush /C1
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Location: /C1/C2
I/flutter (11158): /C didPush /C2
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Location: /B1
I/flutter (11158): /B didPush /B1
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Location: /B1
I/flutter (11158): Stack A - 3 onPopInvokedWithResult didPop=true
I/flutter (11158): /A didPop /A3 -> /A2
I/flutter (11158): Location: /B1
I/flutter (11158): Stack C - 2 onPopInvokedWithResult didPop=true
I/flutter (11158): /C didPop /C2 -> /C1
I/flutter (11158): Location: /B1
I/flutter (11158): Stack A - 2 onPopInvokedWithResult didPop=true
I/flutter (11158): /A didPop /A2 -> /A1
I/flutter (11158): Location: /A1/A2/A3
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Stack A - 3 dispose
I/flutter (11158): Stack A - 2 dispose
I/flutter (11158): Location: /C1/C2
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): android.app.Activity$$ExternalSyntheticLambda0@812848b
D/WindowOnBackDispatcher(11158): setTopOnBackInvokedCallback (unwrapped): io.flutter.embedding.android.FlutterActivity$1@71f9c47
I/flutter (11158): Stack C - 2 dispose

Flutter Doctor output

Doctor output
[✓] Flutter (Channel stable, 3.44.0, on macOS 26.5.1 25F80 darwin-arm64, locale en-AU) [329ms]
    • Flutter version 3.44.0 on channel stable at /Users/dev/development/flutter
    • Upstream repository https://github.com/flutter/flutter.git
    • Framework revision 559ffa3f75 (4 weeks ago), 2026-05-15 14:13:13 -0700
    • Engine revision 4c525dac5e
    • Dart version 3.12.0
    • 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.1.0) [978ms]
    • Android SDK at /Users/dev/Library/Android/sdk
    • Emulator version 36.5.11.0 (build_id 15261927) (CL:N/A)
    • Platform android-36.1, build-tools 36.1.0
    • 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.10+-117844308-b1163.108)
    • All Android licenses accepted.

[✓] Xcode - develop for iOS and macOS (Xcode 26.5) [694ms]
    • Xcode at /Applications/Xcode.app/Contents/Developer
    • Build 17F42
    • CocoaPods version 1.16.2

[✓] Chrome - develop for the web [4ms]
    • Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome

[✓] Connected device (5 available) [5.9s]
    • sdk gphone64 arm64 (mobile)    • emulator-5554                        • android-arm64  • Android 16 (API 36)
      (emulator)
    • Dev’s iPad (wireless) (mobile) • 00008020-000E3D682228402E            • ios            • iOS 26.5 23F77
    • iPhone 17 Pro Max (mobile)     • E44E154F-423F-4325-B615-4C4F9EB67251 • ios            •
      com.apple.CoreSimulator.SimRuntime.iOS-26-5 (simulator)
    • macOS (desktop)                • macos                                • darwin-arm64   • macOS 26.5.1 25F80
      darwin-arm64
    • Chrome (web)                   • chrome                               • web-javascript • Google Chrome
      149.0.7827.104

[✓] Network resources [438ms]
    • All expected network resources are available.

• No issues found!

Edit: add go_router version

go_router version

version: 17.3.0

Activity

  1. danyalahmed1995 commented on Jun 15, 2026

    @danyalahmed1995

    I was able to reproduce this on current flutter/packages.

    While the active location stays /B1, system/predictive back still pops inactive branch routes:

    /A didPop /A3 -> /A2
    /C didPop /C2 -> /C1
    /A didPop /A2 -> /A1
    

    So the issue appears to be inactive StatefulShellBranch navigators being included in system back handling.

  2. danyalahmed1995 commented on Jun 15, 2026

    @danyalahmed1995

    Opened PR at flutter/packages#11910

    The test covers the reproduced case where system/predictive back runs while branch B is active and verifies inactive A/C branch navigators are not popped.

  3. saurabh-mirajkar commented on Jun 16, 2026

    @saurabh-mirajkar
    Contributor

    @jasaw Thanks for the detailed report!

    I was able to reproduce this behavior with the provided sample on the latest stable channel using an Android 16 emulator with predictive back enabled.

    Based on the reproduction:

    • Performing a predictive back gesture while on branch B causes routes in the inactive StatefulShellBranch stacks (branches A and C) to be popped unexpectedly.
    • These affected routes are not disposed of immediately, instead their dispose callbacks are invoked later when those branches are revisited.
    • The disposal order also differs from a normal pop sequence.

    Labeling this for team-android and adding fyi-ecosystem, as this directly affects the first-party go_router package.

  4. added
    packageflutter/packages repository. See also p: labels.
    has reproducible stepsThe issue has been confirmed reproducible and is ready to work on
    team-androidOwned by Android platform team
    fyi-ecosystemFor the attention of Ecosystem team
    and removed
    in triagePresently being triaged by the triage team
    on Jun 16, 2026
  5. added
    fyi-frameworkFor the attention of Framework team
    and removed
    fyi-ecosystemFor the attention of Ecosystem team
    on Jun 23, 2026
  6. added
    P2Important issues not at the top of the work list
    triaged-androidTriaged by Android platform team
    on Jun 23, 2026
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.44Found to occur in 3.44has reproducible stepsThe issue has been confirmed reproducible and is ready to work onp: go_routerThe go_router packagepackageflutter/packages repository. See also p: labels.team-androidOwned by Android platform teamtriaged-androidTriaged by Android platform teamwaiting for PR to land (fixed)A fix is in flight

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions