You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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),
          ],
        ),
      );
    });
  }
}

问题原因

  1. 路由重建与页面注册表冲突:Firebase Auth状态变化时,authenticationListenerProvider状态更新会触发goRouterProvider重建新的GoRouter实例。此时旧实例管理的页面仍在导航栈中,但新实例的页面注册表无这些页面记录,导致断言失败。
  2. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.06 22:25:19