使用GoRouter ShellRoute时,Firebase登录/登出触发registry断言错误
Flutter中GoRouter ShellRoute配合Firebase登录/登出触发断言错误问题
问题现象
在使用带ShellRoute的GoRouter实现全局侧边导航栏的Flutter项目中,调用Firebase登录或登出功能时,触发断言错误:Assertion failed: registry.containsKey(page) is not true
核心代码片段
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
路由与Auth状态管理代码
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); 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); Future<void> _initializeFlutterFire() async { if (_kTestingCrashlytics) { await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(true); } else { await FirebaseCrashlytics.instance .setCrashlyticsCollectionEnabled(!kDebugMode); } Function? originalOnError = FlutterError.onError; FlutterError.onError = (FlutterErrorDetails errorDetails) async { await FirebaseCrashlytics.instance.recordFlutterError(errorDetails); 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), ], ), ); }); } }
问题原因
- 路由重建与页面注册表冲突:Firebase Auth状态变化时,
authenticationListenerProvider状态更新会触发goRouterProvider重建新的GoRouter实例。此时旧实例管理的页面仍在导航栈中,但新实例的页面注册表无这些页面记录,导致断言失败。 - ShellRoute下的登录页面归属问题:登录页面嵌套在ShellRoute内部,登录成功跳转时,Shell导航栈与根导航栈易出现管理混乱,Auth状态变化触发路由重建时,旧页面无法被新路由识别。
解决方案
方案1:将登录页面移出ShellRoute,使用根导航器
把登录页面作为根级GoRoute,脱离ShellRoute的嵌套,避免导航栈干扰:
final goRouterProvider = Provider<GoRouter>((ref) { final auth = ref.watch(authenticationListenerProvider); return GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: auth ? '/home' : '/login', routes: <RouteBase>[ // 登录页面作为根路由,使用根导航器 GoRoute( path: '/login', parentNavigatorKey: _rootNavigatorKey, 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'); }), ], )); }, ), /// 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: '/account', redirect: ((context, state) { if (!auth) { 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()); }, ), ], ), ], // 全局重定向处理Auth状态 redirect: (context, state) { final isLoggedIn = auth; final isGoingToLogin = state.location == '/login'; if (!isLoggedIn && !isGoingToLogin) return '/login'; if (isLoggedIn && isGoingToLogin) return '/home'; return null; }, ); });
方案2:避免路由实例频繁重建
通过ref.listen监听Auth状态变化,手动触发路由跳转,而非重建整个GoRouter:
final goRouterProvider = Provider<GoRouter>((ref) { final router = 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>[ // 保留原路由配置 ], ), ], ); // 监听Auth状态变化,手动跳转 ref.listen(authenticationListenerProvider, (prev, curr) { if (curr == true && router.location == '/login') { router.go('/home'); } else if (curr == false && router.location != '/login') { router.go('/login'); } }); return router; });
方案3:修复登出逻辑,确保路由跳转同步
修改登出方法,在登出完成后手动跳转登录页,避免状态变化导致的冲突:
void _onItemTapped(int index, bool auth, BuildContext context) { switch (index) { // 其他case保持不变 case 3: if (auth == true) { FirebaseAuthService.signOut().then((_) { GoRouter.of(context).go('/login'); }); } else { GoRouter.of(context).go('/login'); } break; } }
内容的提问来源于stack exchange,提问作者Andrew68
相关产品推荐
相关产品推荐

