bizz84 / nested_navigation_examples

Bottom Navigation Bar with Nested Routes: GoRouter vs Beamer
https://codewithandrea.com/
MIT License
105 stars 18 forks source link

Cupertino navigation fails with "The key [<StatefulNavigationShellState>] was used by multiple widgets" #11

Open kewur opened 1 month ago

kewur commented 1 month ago

I know there was a bug fix that was related to this, the workaround for that was making gorouter a global variable. However, I'm not using MaterialApp, but cuportino app. After switching everything over, when I navigate between tabs, I the screen crashes and I get this error.

The key [LabeledGlobalKey<StatefulNavigationShellState>#3d108] was used by multiple widgets. The parents of those widgets were:
- Semantics(container: false, properties: SemanticsProperties, renderObject: RenderSemanticsAnnotations#f9f71)
- Semantics(container: false, properties: SemanticsProperties, renderObject: RenderSemanticsAnnotations#cc9b7)
A GlobalKey can only be specified on one widget at a time in the widget tree.
When the exception was thrown, this was the stack: 
#0      BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous closure>.<anonymous closure> (package:flutter/src/widgets/framework.dart:3113:13)
#1      _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
#2      BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure>.<anonymous closure> (package:flutter/src/widgets/framework.dart:3057:20)
#3      _LinkedHashMapMixin.forEach (dart:collection-patch/compact_hash.dart:633:13)
#4      BuildOwner._debugVerifyGlobalKeyReservation.<anonymous closure> (package:flutter/src/widgets/framework.dart:3052:36)
#5      BuildOwner._debugVerifyGlobalKeyReservation (package:flutter/src/widgets/framework.dart:3121:6)
#6      BuildOwner.finalizeTree.<anonymous closure> (package:flutter/src/widgets/framework.dart:3178:11)
#7      BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3260:8)

this is the same example in Cuportino form

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

// ignore: depend_on_referenced_packages
import 'package:flutter_web_plugins/url_strategy.dart';

// private navigators
final _rootNavigatorKey = GlobalKey<NavigatorState>();
final _shellNavigatorAKey = GlobalKey<NavigatorState>(debugLabel: 'shellA');
final _shellNavigatorBKey = GlobalKey<NavigatorState>(debugLabel: 'shellB');

final goRouter = GoRouter(
  initialLocation: '/a',
  // * Passing a navigatorKey causes an issue on hot reload:
  // * https://github.com/flutter/flutter/issues/113757#issuecomment-1518421380
  // * However it's still necessary otherwise the navigator pops back to
  // * root on hot reload
  navigatorKey: _rootNavigatorKey,
  debugLogDiagnostics: true,
  routes: [
    // Stateful navigation based on:
    // https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
    StatefulShellRoute.indexedStack(
      builder: (context, state, navigationShell) {
        return ScaffoldWithNestedNavigation(navigationShell: navigationShell);
      },
      branches: [
        StatefulShellBranch(
          navigatorKey: _shellNavigatorAKey,
          routes: [
            GoRoute(
              path: '/a',
              pageBuilder: (context, state) => const NoTransitionPage(
                child: RootScreen(label: 'A', detailsPath: '/a/details'),
              ),
              routes: [
                GoRoute(
                  path: 'details',
                  builder: (context, state) => const DetailsScreen(label: 'A'),
                ),
              ],
            ),
          ],
        ),
        StatefulShellBranch(
          navigatorKey: _shellNavigatorBKey,
          routes: [
            // Shopping Cart
            GoRoute(
              path: '/b',
              pageBuilder: (context, state) => const NoTransitionPage(
                child: RootScreen(label: 'B', detailsPath: '/b/details'),
              ),
              routes: [
                GoRoute(
                  path: 'details',
                  builder: (context, state) => const DetailsScreen(label: 'B'),
                ),
              ],
            ),
          ],
        ),
      ],
    ),
  ],
);

void main() {
  // turn off the # in the URLs on the web
  usePathUrlStrategy();
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return CupertinoApp.router(
      routerConfig: goRouter,
      debugShowCheckedModeBanner: false,
      theme: const CupertinoThemeData(),
    );
  }
}

// Stateful navigation based on:
// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart
class ScaffoldWithNestedNavigation extends StatelessWidget {
  const ScaffoldWithNestedNavigation({
    Key? key,
    required this.navigationShell,
  }) : super(
            key: key ?? const ValueKey<String>('ScaffoldWithNestedNavigation'));
  final StatefulNavigationShell navigationShell;

  void _goBranch(int index) {
    navigationShell.goBranch(
      index,
      // A common pattern when using bottom navigation bars is to support
      // navigating to the initial location when tapping the item that is
      // already active. This example demonstrates how to support this behavior,
      // using the initialLocation parameter of goBranch.
      initialLocation: index == navigationShell.currentIndex,
    );
  }

  @override
  Widget build(BuildContext context) {
    return CupertinoTabScaffold(
      tabBar: CupertinoTabBar(
        currentIndex: navigationShell.currentIndex,
        items: const [
          BottomNavigationBarItem(label: 'Section A', icon: Icon(CupertinoIcons.home)),
          BottomNavigationBarItem(label: 'Section B', icon: Icon(CupertinoIcons.settings)),
        ],
        onTap: _goBranch,
      ),
      tabBuilder: (context, index) {
        return CupertinoTabView(
          builder: (context) {
            return navigationShell; // Assuming navigationShell is a widget that can handle navigation.
          },
        );
      },
    );
  }
}

/// Widget for the root/initial pages in the bottom navigation bar.
class RootScreen extends StatelessWidget {
  /// Creates a RootScreen
  const RootScreen({required this.label, required this.detailsPath, Key? key})
      : super(key: key);

  /// The label
  final String label;

  /// The path to the detail page
  final String detailsPath;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Tab root - $label'),
      ),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text('Screen $label',
                style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle),
            const Padding(padding: EdgeInsets.all(4)),
            CupertinoButton(
              onPressed: () => context.go(detailsPath),
              child: const Text('View details'),
            ),
          ],
        ),
      ),
    );
  }
}

/// The details screen for either the A or B screen.
class DetailsScreen extends StatefulWidget {
  /// 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
  State<StatefulWidget> createState() => DetailsScreenState();
}

/// The state for DetailsScreen
class DetailsScreenState extends State<DetailsScreen> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    return CupertinoPageScaffold(
      navigationBar: CupertinoNavigationBar(
        middle: Text('Details Screen - ${widget.label}'),
      ),
      child: Center(
        child: Column(
          mainAxisSize: MainAxisSize.min,
          children: <Widget>[
            Text('Details for ${widget.label} - Counter: $_counter',
                style: CupertinoTheme.of(context).textTheme.navLargeTitleTextStyle),
            const Padding(padding: EdgeInsets.all(4)),
            CupertinoButton(
              onPressed: () {
                setState(() {
                  _counter++;
                });
              },
              child: const Text('Increment counter'),
            ),
          ],
        ),
      ),
    );
  }
}

any idea as to why this works on material but not on cuportino?

kewur commented 1 month ago

looks to be the same issue https://github.com/flutter/flutter/issues/113757 but havent found a workaround yet

RCVZ commented 1 day ago

I got the same error when using pageBuilder with a custom transition page. I solved it by adding the state.pageKey to the custom transition page.

You could try this:

        StatefulShellRoute.indexedStack(
          builder: (context, state, navigationShell) {
            return ScaffoldWithNestedNavigation(
              key: state.pageKey,
              navigationShell: navigationShell,
            );
          },
          // the rest of your code
        ),