skip to Main Content

I’m using a basic GoRouter with shellroute setup with a side navbar that is meant to remain consistent across pages. Both a login or logout call to Firebase will generate the assertion, but I don’t understand why? Any pointers would be appreciated. Code below:

Pubspec:

  flutter:
    sdk: flutter
  firebase_core: ^2.1.0
  firebase_auth: ^4.0.2
  firebase_storage: ^11.0.2
  firebase_crashlytics: ^3.0.2
  firebase_analytics: ^10.0.2
  flutter_riverpod: ^2.0.2
  cloud_firestore: ^4.0.2
  intl: ^0.17.0
  equatable: ^2.0.3
  google_sign_in: ^5.0.7
  sign_in_with_apple: ^4.1.0
  crypto: ^3.0.1
  rxdart: ^0.27.1
  flutter_form_builder: ^7.7.0
  form_builder_validators: ^8.3.0
  logger: ^1.0.0 
  shared_preferences: ^2.0.7
  google_fonts: ^3.0.1
  package_info_plus: ^1.0.6
  responsive_framework: ^0.2.0
  flex_color_scheme: ^6.0.1
  go_router: ^6.0.0

top-level providers:

final GlobalKey<NavigatorState> _rootNavigatorKey =
    GlobalKey(debugLabel: 'root');
final GlobalKey<NavigatorState> _shellNavigatorKey =
    GlobalKey(debugLabel: 'shell');

final providers = [EmailAuthProvider()];

final firebaseAuthService = Provider<FirebaseAuthService>(
    (ref) => FirebaseAuthService(FirebaseAuth.instance));


class AuthenticationNotifier extends StateNotifier<bool> {
  AuthenticationNotifier(this._authenticationRepository) : super(false) {
    _authenticationRepository.firebaseAuth.authStateChanges().listen((user) {
      if (user == null) {
        state = false;
      } else {
        state = true;
      }
    });
  }

  final FirebaseAuthService _authenticationRepository;
}

final authenticationListenerProvider =
    StateNotifierProvider<AuthenticationNotifier, bool>(
  (ref) => AuthenticationNotifier(ref.watch(firebaseAuthService)),
);

final goRouterProvider = Provider<GoRouter>((ref) {
  final auth = ref.watch(authenticationListenerProvider);

  return GoRouter(
    navigatorKey: _rootNavigatorKey,
    initialLocation: '/home',
    routes: <RouteBase>[
      /// Application shell
      ShellRoute(
        navigatorKey: _shellNavigatorKey,
        builder: (BuildContext context, GoRouterState state, Widget child) {
          return ScaffoldWithNavBar(child: child);
        },
        routes: <RouteBase>[
          GoRoute(
            path: '/',
            pageBuilder: (BuildContext context, GoRouterState state) {
              return NoTransitionPage(child: HomePage());
            },
          ),
          GoRoute(
            path: '/home',
            pageBuilder: (BuildContext context, GoRouterState state) {
              return NoTransitionPage(child: HomePage());
            },
          ),
          GoRoute(
            path: '/login',
            pageBuilder: (BuildContext context, GoRouterState state) {
              return NoTransitionPage(
                  child: SignInScreen(
                providers: providers,
                actions: [
                  AuthStateChangeAction<SignedIn>((context, state) {
                    if (state.user != null) GoRouter.of(context).go('/home');
                  }),
                ],
              ));
            },
          ),
          GoRoute(
            path: '/account',
            redirect: ((context, state) {
              if (auth == false) {
                return '/login';
              } else {
                return null;
              }
            }),
            pageBuilder: (BuildContext context, GoRouterState state) {
              return NoTransitionPage(child: AccountPage());
            },
          ),
          GoRoute(
            path: '/surveys',
            pageBuilder: (BuildContext context, GoRouterState state) {
              return NoTransitionPage(child: SurveyPage());
            },
          ),
        ],
      ),
    ],
  );
});

class ScaffoldWithNavBar extends ConsumerWidget {
  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;
  int selectedIndex = 0;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final auth = ref.watch(authenticationListenerProvider);
    return Scaffold(
      body: Row(
        children: [
          NavigationRail(
            selectedIndex: _calculateSelectedIndex(context),
            onDestinationSelected: ((value) =>
                _onItemTapped(value, auth, context)),
            labelType: NavigationRailLabelType.all,
            destinations: [
              const NavigationRailDestination(
                icon: Icon(Icons.home),
                label: Text('Home'),
              ),
              const NavigationRailDestination(
                icon: Icon(Icons.account_box),
                label: Text('Account'),
              ),
              const NavigationRailDestination(
                icon: Icon(Icons.access_alarm),
                label: Text('Surveys'),
              ),
              if (auth == false)
                NavigationRailDestination(
                    label: Text('SignIn'), icon: Icon(Icons.accessibility_new)),
              if (auth == true)
                NavigationRailDestination(
                    label: Text('SignOut'),
                    icon: Icon(Icons.add_circle_outline_outlined))
            ],
          ),
          Expanded(child: child)
        ],
      ),
    );
  }

  static int _calculateSelectedIndex(BuildContext context) {
    final String location = GoRouterState.of(context).location;
    if (location.startsWith('/home')) {
      return 0;
    }
    if (location.startsWith('/account')) {
      return 1;
    }
    if (location.startsWith('/surveys')) {
      return 2;
    }
    if (location.startsWith('/login')) {
      return 3;
    }
    return 0;
  }

  void _onItemTapped(int index, bool auth, BuildContext context) {
    switch (index) {
      case 0:
        GoRouter.of(context).go('/home');
        break;
      case 1:
        GoRouter.of(context).go('/account');
        break;
      case 2:
        GoRouter.of(context).go('/surveys');
        break;
      case 3:
        if (auth == true) {
          FirebaseAuthService.signOut();
        } else {
          GoRouter.of(context).go('/login');
        }
        break;
    }
  }
}

main.dart


void main() async {
  runZonedGuarded(() async {
    WidgetsFlutterBinding.ensureInitialized();
    await Firebase.initializeApp(
      options: DefaultFirebaseOptions.currentPlatform,
    );
    final sharedPreferences = await SharedPreferences.getInstance();
    runApp(ProviderScope(overrides: [
      sharedPreferencesServiceProvider.overrideWithValue(
        SharedPreferencesService(sharedPreferences),
      ),
    ], child: MyApp()));
  },
      ((error, stack) =>
          FirebaseCrashlytics.instance.recordError(error, stack)));
}

class MyApp extends ConsumerWidget {
  MyApp({Key? key}) : super(key: key);

  // Define an async function to initialize FlutterFire
  Future<void> _initializeFlutterFire() async {
    // Wait for Firebase to initialize

    if (_kTestingCrashlytics) {
      // Force enable crashlytics collection enabled if we're testing it.
      await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true);
    } else {
      // Else only enable it in non-debug builds.
      // You could additionally extend this to allow users to opt-in.
      await FirebaseCrashlytics.instance
          .setCrashlyticsCollectionEnabled(!kDebugMode);
    }

    // Pass all uncaught errors to Crashlytics.
    Function? originalOnError = FlutterError.onError;
    FlutterError.onError = (FlutterErrorDetails errorDetails) async {
      await FirebaseCrashlytics.instance.recordFlutterError(errorDetails);
      // Forward to original handler.
      originalOnError!(errorDetails);
    };
  }

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    if (!kIsWeb) _initializeFlutterFire();

    return Consumer(builder: (context, ref, child) {
      final theme = ref.watch(themeProvider);
      final router = ref.watch(goRouterProvider);

      return MaterialApp.router(
        routerConfig: router,
        theme: theme[0],
        darkTheme: theme[1],
        themeMode: ThemeMode.system,
        debugShowCheckedModeBanner: false,
        builder: (context, widget) => ResponsiveWrapper.builder(
          ClampingScrollWrapper.builder(
            context,
            widget!,
          ),
          minWidth: 480,
          defaultScale: true,
          breakpoints: [
            ResponsiveBreakpoint.resize(480, name: MOBILE),
            ResponsiveBreakpoint.autoScale(800, name: TABLET),
            ResponsiveBreakpoint.resize(1000, name: DESKTOP),
          ],
        ),
      );
    });
  }
}

Error on login or logout:

Assertion failed: registry.containskey(page) is not true.

2

Answers


  1. Chosen as BEST ANSWER

    I worked out what was wrong - need to use PageBuilder with Shellroute rather than builder for NavBar. Then worked fine. Hope this helps someone else.

    return GoRouter(
        navigatorKey: _rootNavigatorKey,
        initialLocation: '/home',
        routes: <RouteBase>[
          /// Application shell
          ShellRoute(
            navigatorKey: _shellNavigatorKey,
            pageBuilder: (BuildContext context, GoRouterState state, Widget child) {
              return NoTransitionPage(child: ScaffoldWithNavBar(child: child));
            },
            routes:[]
    

  2. I’ve also had this error, in my case it was thrown on every hot reload and hot restart of the app.

    After looking into the stack trace of the error I found out that it’s caused by multiple widgets using the same global key (which should never happen, as keys are supposed to uniquely identify elements).

    In your case, the global keys for navigators:

    final GlobalKey<NavigatorState> _rootNavigatorKey =
        GlobalKey(debugLabel: 'root');
    final GlobalKey<NavigatorState> _shellNavigatorKey =
        GlobalKey(debugLabel: 'shell');
    

    are global variables and are reused when Flutter builds new instances of GoRouter on reload. The solution is to move them inside the router generator function, as here:

    final goRouterProvider = Provider<GoRouter>((ref) {
      final auth = ref.watch(authenticationListenerProvider);
    
      // MOVE HERE
      final GlobalKey<NavigatorState> _rootNavigatorKey =
        GlobalKey(debugLabel: 'root');
      final GlobalKey<NavigatorState> _shellNavigatorKey =
        GlobalKey(debugLabel: 'shell');
    
      return GoRouter(
        navigatorKey: _rootNavigatorKey,
      // ...
    

    Now new keys will be generated for new instances of GoRouter and the key conflict is gone.

    Also, if you want to store your GoRouter object as a global variable, create a function that creates the object (with the global keys as variables inside the function) and create a global variable from this function, like in this example:

    final GoRouter router = createRouter();
    
    GoRouter createRouter() {
      final rootNavigatorKey = GlobalKey<NavigatorState>();
      final shellNavigatorKey = GlobalKey<NavigatorState>();
    
      return GoRouter(
        navigatorKey: rootNavigatorKey,
        // ...
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search