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
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.
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:
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: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: