使用GoRouter StatefulShellRoute结合redirect时出现Duplicate GlobalKey错误
在StatefulShellRoute中使用GoRouter的redirect导致Duplicate GlobalKey错误
问题描述
在Flutter应用中使用GoRouter的StatefulShellRoute实现底部导航栏,结合redirect做登录态校验时,触发了Duplicate GlobalKey错误。以下是可在DartPad中复现的最小代码:
import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; class LoggedinNotifier extends StateNotifier<bool> { LoggedinNotifier() : super(false); Future<void> set(bool val) async { await Future.delayed(const Duration(seconds: 1)); state = val; } } final loggedInProvider = StateNotifierProvider<LoggedinNotifier, bool>((ref) => LoggedinNotifier()); class NavigationKeys { static final rootNavigatorKey = GlobalKey<NavigatorState>(); static final homeNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'Home'); static final profileNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'Profile'); } enum AppRoutes { home, profile, profileLoggedIn, profileLogin, } final appRouterProvider = Provider<GoRouter>((ref) { final loggedIn = ref.watch(loggedInProvider); return GoRouter( debugLogDiagnostics: true, initialLocation: '/home', routes: [ StatefulShellRoute.indexedStack( branches: [ StatefulShellBranch(routes: [ GoRoute( path: '/home', name: AppRoutes.home.name, builder: (context, state) => const Scaffold( body: Center(child: Text('Home')), ), ) ], navigatorKey: NavigationKeys.homeNavigatorKey), StatefulShellBranch(routes: [ GoRoute( path: '/profile', name: AppRoutes.profile.name, builder: (context, state) { return Consumer( builder: (context, ref, child) => TextButton( onPressed: () async { await ref.read(loggedInProvider.notifier).set(false); }, child: const Text('Logout')), ); }, routes: [ GoRoute( path: 'authentication/login', name: AppRoutes.profileLogin.name, builder: (context, state) { return Consumer( builder: (context, ref, child) => TextButton( onPressed: () async { await ref .read(loggedInProvider.notifier) .set(true); }, child: const Text('Login')), ); }, ), ]) ], navigatorKey: NavigationKeys.profileNavigatorKey), ], builder: (context, state, navigationShell) => Scaffold( body: navigationShell, bottomNavigationBar: BottomNavigationBar( enableFeedback: true, type: BottomNavigationBarType.fixed, currentIndex: navigationShell.currentIndex, onTap: (value) { navigationShell.goBranch( value, ); }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home_outlined), label: 'Home'), BottomNavigationBarItem( icon: Icon(Icons.person_outline), label: 'Profile') ]), ), ) ], navigatorKey: NavigationKeys.rootNavigatorKey, redirect: (context, state) { if (!loggedIn && !state.uri.toString().contains('/authentication')) { return state.namedLocation(AppRoutes.profileLogin.name); } return null; }, ); }); void main() { runApp(const ProviderScope(child: MyApp())); } class MyApp extends ConsumerWidget { const MyApp({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return MaterialApp.router(routerConfig: ref.watch(appRouterProvider)); } }
复现步骤
- 将代码复制到DartPad运行
- 点击Login按钮
- 切换到Profile标签页
- 点击Logout按钮
报错信息
检测到Widget树中存在重复的GlobalKey。 Widget树中多次指定了以下GlobalKey。这会导致Widget树的部分被意外截断,因为当第二次遇到同一个Key时,之前的实例会被移动到新位置。该Key为: - [GlobalObjectKey int#b8c6a] 通过观察发现,带有上述全局Key的Widget被移出其父Widget后,该父Widget在当前帧中从未更新,这意味着它要么完全没有更新,要么在Widget移动前就已更新,无论哪种情况都表明它仍然认为自己应该拥有一个带有该全局Key的子Widget。 因GlobalKey重父化而被强制移除一个或多个子Widget后未更新的特定父Widget是: - _BranchNavigatorProxy-[StatefulShellBranch#a507a](state: _BranchNavigatorProxyState#1f5f1) 一个GlobalKey在Widget树中同一时间只能指定给一个Widget。 抛出异常时的调用栈: #0 BuildOwner.finalizeTree.<anonymous closure> (package:flutter/src/widgets/framework.dart:3317:15) framework.dart:3317 #1 BuildOwner.finalizeTree (package:flutter/src/widgets/framework.dart:3342:8) framework.dart:3342 #2 WidgetsBinding.drawFrame (package:flutter/src/widgets/binding.dart:1169:19) binding.dart:1169 #3 RendererBinding._handlePersistentFrameCallback (package:flutter/src/rendering/binding.dart:468:5) binding.dart:468 #4 SchedulerBinding._invokeFrameCallback (package:flutter/src/scheduler/binding.dart:1397:15) binding.dart:1397 #5 SchedulerBinding.handleDrawFrame (package:flutter/src/scheduler/binding.dart:1318:9) binding.dart:1318 #6 SchedulerBinding._handleDrawFrame (package:flutter/src/scheduler/binding.dart:1176:5) binding.dart:1176 #7 _invoke (dart:ui/hooks.dart:312:13) hooks.dart:312 #8 PlatformDispatcher._drawFrame (dart:ui/platform_dispatcher.dart:419:5) platform_dispatcher.dart:419 #9 _drawFrame (dart:ui/hooks.dart:283:31) hooks.dart:283 ════════════════════════════════════════════════════════════════════════════════
问题分析
错误核心原因是:登录状态变化触发全局redirect跳转时,StatefulShellRoute的分支导航器GlobalKey被重复绑定。Logout后redirect跳转到登录页时,Profile分支的旧导航器实例未被正确销毁,仍持有该GlobalKey,而新的导航实例又尝试复用同一个Key,导致冲突。同时,登录页作为Profile分支的子路由,全局redirect会干扰StatefulShellRoute内部的导航栈管理,进一步引发Key重复问题。
解决方案
方案1:将登录校验逻辑移至分支路由内部
移除全局redirect,在Profile分支的根路由内处理登录态校验,避免全局跳转干扰分支导航器状态:
final appRouterProvider = Provider<GoRouter>((ref) { final loggedIn = ref.watch(loggedInProvider); return GoRouter( debugLogDiagnostics: true, initialLocation: '/home', routes: [ // 将登录页改为根路由的子路由,使用根导航器 GoRoute( path: '/login', name: AppRoutes.profileLogin.name, builder: (context, state) { return Consumer( builder: (context, ref, child) => TextButton( onPressed: () async { await ref.read(loggedInProvider.notifier).set(true); context.goNamed(AppRoutes.profile.name); }, child: const Text('Login')), ); }, ), StatefulShellRoute.indexedStack( branches: [ StatefulShellBranch(routes: [ GoRoute( path: '/home', name: AppRoutes.home.name, builder: (context, state) => const Scaffold( body: Center(child: Text('Home')), ), ) ], navigatorKey: NavigationKeys.homeNavigatorKey), StatefulShellBranch(routes: [ GoRoute( path: '/profile', name: AppRoutes.profile.name, // 在Profile路由内部做登录校验 redirect: (context, state) { if (!loggedIn) { return '/login'; } return null; }, builder: (context, state) { return Consumer( builder: (context, ref, child) => TextButton( onPressed: () async { await ref.read(loggedInProvider.notifier).set(false); context.goNamed(AppRoutes.profileLogin.name); }, child: const Text('Logout')), ); },) ], navigatorKey: NavigationKeys.profileNavigatorKey), ], builder: (context, state, navigationShell) => Scaffold( body: navigationShell, bottomNavigationBar: BottomNavigationBar( enableFeedback: true, type: BottomNavigationBarType.fixed, currentIndex: navigationShell.currentIndex, onTap: (value) { navigationShell.goBranch( value, ); }, items: const [ BottomNavigationBarItem( icon: Icon(Icons.home_outlined), label: 'Home'), BottomNavigationBarItem( icon: Icon(Icons.person_outline), label: 'Profile') ]), ), ) ], navigatorKey: NavigationKeys.rootNavigatorKey, ); });
方案2:避免全局redirect跳转至分支内子路由
全局redirect时,优先跳转到分支的根路由,再由根路由内部跳转至登录页,或直接使用根导航器承载登录页,避免干扰分支导航器的Key管理。
总结
核心问题是全局redirect与StatefulShellRoute的分支导航器Key管理冲突,通过将登录校验逻辑移至分支路由内部,或调整路由结构让登录页脱离分支导航器,即可解决Duplicate GlobalKey错误。
内容的提问来源于stack exchange,提问作者SHUVAM JASWAL
相关产品推荐
相关产品推荐

