Flutter Web中GoRouter+ShellRoute的Duplicate GlobalKey问题修复求助
修复Flutter Web中Riverpod+GoRouter ShellRoute重复点击同一链接导致空白页与Duplicate GlobalKey异常问题
问题描述
在使用Riverpod和GoRouter的Flutter Web应用中,采用ShellRoute实现主内容片段显示后,功能符合预期,但桌面视图下重复点击左侧菜单的同一链接时,首次跳转正常,第二次页面变为空白,同时捕获到Duplicate GlobalKey异常,移动端无此问题。
异常信息
══╡ EXCEPTION CAUGHT BY WIDGETS LIBRARY ╞═══════════════════════════════════════════════════════════ The following assertion was thrown while finalizing the widget tree: Duplicate GlobalKey detected in widget tree. The following GlobalKey was specified multiple times in the widget tree. This will lead to parts of the widget tree being truncated unexpectedly, because the second time a key is seen, the previous instance is moved to the new location. The key was: - [GlobalObjectKey int#ab910] This was determined by noticing that after the widget with the above global key was moved out of its previous parent, that previous parent never updated during this frame, meaning that it either did not update at all or updated before the widget was moved, in either case implying that it still thinks that it should have a child with that global key. The specific parent that did not update after having one or more children forcibly removed due to GlobalKey reparenting is: - Expanded(flex: 4) A GlobalKey can only be specified on one widget at a time in the widget tree. When the exception was thrown, this was the stack: dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/errors.dart 296:3 throw_ packages/flutter/src/widgets/framework.dart 3235:15 <fn> packages/flutter/src/widgets/framework.dart 3259:16 finalizeTree packages/flutter/src/widgets/binding.dart 1143:7 drawFrame packages/flutter/src/rendering/binding.dart 443:5 [_handlePersistentFrameCallback] packages/flutter/src/scheduler/binding.dart 1392:7 [_invokeFrameCallback] packages/flutter/src/scheduler/binding.dart 1313:9 handleDrawFrame packages/flutter/src/scheduler/binding.dart 1171:5 [_handleDrawFrame] lib/_engine/engine/platform_dispatcher.dart 1404:5 invoke lib/_engine/engine/platform_dispatcher.dart 307:5 invokeOnDrawFrame lib/_engine/engine/initialization.dart 187:36 <fn> dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 426:37 _checkAndCall dart-sdk/lib/_internal/js_dev_runtime/private/ddc_runtime/operations.dart 431:39 dcall
相关代码
Router Provider
final _rootkey = GlobalKey<NavigatorState>(debugLabel: 'root'); final _shellKey = GlobalKey<NavigatorState>(debugLabel: 'shell'); final routerProvider = Provider<GoRouter>((ref) { final authState = ref.watch(authProvider); return GoRouter( navigatorKey: _rootkey, debugLogDiagnostics: false, initialLocation: '/', refreshListenable: authState, routes: [ GoRoute( path: '/login', name: LoginPage.routeName, builder: (context, state) { return const LoginPage(); }), GoRoute( path: '/cover', name: CoverPage.routeName, builder: (context, state) { return const CoverPage(); }), ShellRoute( navigatorKey: _shellKey, builder: (context, state, child) => ShellPage(child: child), routes: [ GoRoute( path: '/', name: HomePage.routeName, parentNavigatorKey: _shellKey, builder: (context, state) { return const HomePage(); }, ), GoRoute( path: '/viewUsers', name: ViewUsers.routeName, parentNavigatorKey: _shellKey, builder: (context, state) { return const ViewUsers(); }), GoRoute( path: '/addUser', name: AddUser.routeName, parentNavigatorKey: _shellKey, builder: (context, state) { return const AddUser(); }), ], ), ], redirect: (context, state) { final isAuthenticated = authState.isLoggedIn; if (state.fullPath == '/login') { return isAuthenticated ? null : '/login'; } return isAuthenticated ? null : '/cover'; }); });
Shell Page
class ShellPage extends ConsumerStatefulWidget { final Widget child; const ShellPage({required this.child, super.key}); @override ConsumerState createState() => _ShellPageState(); } class _ShellPageState extends ConsumerState<ShellPage> { final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); @override Widget build(BuildContext context) { return LayoutBuilder( builder: (context, constraints) { return Scaffold( key: _scaffoldKey, appBar: AppBar( title: const Text('App Title'), actions: <Widget>[ IconButton( onPressed: () { ref.read(authProvider).signOut(); }, icon: const Icon(Icons.logout)) ], leading: constraints.maxWidth > 600 ? null : IconButton( onPressed: () { if (_scaffoldKey.currentState!.isDrawerOpen) { _scaffoldKey.currentState!.openEndDrawer(); } else { _scaffoldKey.currentState!.openDrawer(); } }, icon: const Icon(Icons.menu)), ), body: constraints.maxWidth > 600 ? Row( children: [ const Expanded(flex: 1, child: MenuPanel()), Expanded(flex: 4, child: widget.child), ], ) : widget.child, drawer: constraints.maxWidth > 600 ? null : const MenuPanel(), ); }, ); } }
Menu Panel
class MenuPanel extends ConsumerWidget { const MenuPanel({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final String userAccountType = ref.watch(userProvider.notifier).state!.accountType; final router = ref.watch(routerProvider); return Drawer( child: ListView( children: <Widget>[ if (userAccountType == 'admin') ...[ ListTile( title: const Text("View Users"), onTap: () { router.goNamed(ViewUsers.routeName); Navigator.pop(context); }, ), ListTile( title: const Text("Add Users"), onTap: () { router.goNamed(AddUser.routeName); Navigator.pop(context); }, ), ] else ...[ ListTile( title: const Text("Menu Item"), onTap: () {}, ), ] ], ), ); } }
修复方案
1. 为主内容容器添加路由关联的唯一Key
在ShellPage的桌面视图布局中,给承载主内容的Expanded(flex:4)组件添加基于当前路由URI的ValueKey,确保每次路由变化时父容器的标识唯一,避免GlobalKey冲突:
// 修改ShellPage中Row的第二个Expanded Expanded( flex: 4, key: ValueKey(GoRouterState.of(context).uri), child: widget.child, ),
2. 避免重复跳转同一路由
在MenuPanel的ListTile点击事件中,先判断当前路由是否已经是目标路由,仅在不同路由时执行跳转操作,减少不必要的组件树更新:
// 以ViewUsers的ListTile为例修改onTap逻辑 ListTile( title: const Text("View Users"), onTap: () { final currentRouteName = GoRouterState.of(context).name; if (currentRouteName != ViewUsers.routeName) { router.goNamed(ViewUsers.routeName); } Navigator.pop(context); }, ),
其他菜单项同理修改。
3. 检查页面组件的GlobalKey使用
检查HomePage、ViewUsers、AddUser等页面组件,若存在硬编码的GlobalKey,改为局部Key(如UniqueKey())或动态生成的Key,确保每个组件实例的Key唯一。
方案说明
- 给主内容容器添加路由关联的Key,能让Flutter在路由切换时正确识别组件的身份,避免GlobalKey重复绑定导致的组件树异常。
- 避免重复跳转同一路由,从根源上减少触发异常的场景,同时提升用户体验。
内容的提问来源于stack exchange,提问作者Sheenergizer
相关产品推荐
相关产品推荐

