Flutter+Beamer混合标签导航遇GlobalKey重复及焦点错误
问题:Flutter Beamer 导航中返回键触发 GlobalKey 重复及焦点断言错误
我在Flutter中用Beamer实现了如下导航逻辑:部分路由页面显示底部标签栏,其余路由隐藏标签栏。正常跳转时功能正常,但点击Android返回键时,会抛出「Multiple widgets used the same GlobalKey」和焦点管理相关的断言错误。
错误日志片段
The following assertion was thrown during a scheduler callback: Multiple widgets used the same GlobalKey. The key [GlobalKey<NavigatorState>] was used by multiple widgets. The parents of those widgets were: - _FocusMarker - _FocusMarker ... Another exception was thrown: Assertion failed: file:///.../focus_manager.dart:1442:12 !_debugLosingFocus.contains(node) is not true
问题根源
这种错误通常是因为动态切换显示/隐藏底部导航栏时,同一个GlobalKey被重复绑定到不同的Navigator(或依赖GlobalKey的组件)实例。当返回键触发路由回退时,Beamer重新构建页面树,之前的Navigator实例未被正确销毁,导致GlobalKey冲突;同时焦点管理器因组件树异常变化,触发焦点断言错误。
解决方案
采用Beamer官方推荐的嵌套BeamerDelegate + IndexedStack实现底部导航栏路由,分离带底部栏的路由和全屏路由,避免手动管理GlobalKey带来的冲突。
修复后的完整代码
import 'package:beamer/beamer.dart'; import 'package:flutter/material.dart'; void main() => runApp(const MyApp()); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( routerDelegate: BeamerDelegate( locationBuilder: BeamerLocationBuilder( beamLocations: [ HomeLocation(), DetailsLocation(), ], ), ), routeInformationParser: BeamerParser(), ); } } // 带底部导航栏的主路由组 class HomeLocation extends BeamLocation<BeamState> { @override List<String> get pathPatterns => ['/', '/tab1', '/tab2']; @override List<BeamPage> buildPages(BuildContext context, BeamState state) { final pages = [ BeamPage( key: const ValueKey('home-shell'), child: const HomeShell(), ), if (state.uri.path == '/tab1') BeamPage( key: const ValueKey('tab1-page'), child: const Tab1Page(), ), if (state.uri.path == '/tab2') BeamPage( key: const ValueKey('tab2-page'), child: const Tab2Page(), ), ]; return pages; } } // 无底部导航栏的详情路由组 class DetailsLocation extends BeamLocation<BeamState> { @override List<String> get pathPatterns => ['/details/:id']; @override List<BeamPage> buildPages(BuildContext context, BeamState state) { return [ BeamPage( key: ValueKey('details-${state.pathParameters['id']}'), child: DetailsPage(id: state.pathParameters['id']!), ), ]; } } // 底部导航栏容器(Shell) class HomeShell extends StatefulWidget { const HomeShell({super.key}); @override State<HomeShell> createState() => _HomeShellState(); } class _HomeShellState extends State<HomeShell> { int _currentIndex = 0; final _beamers = [ BeamerDelegate( initialPath: '/tab1', locationBuilder: SimpleLocationBuilder( routes: {'/tab1': (context, state) => const Tab1Page()}, ), ), BeamerDelegate( initialPath: '/tab2', locationBuilder: SimpleLocationBuilder( routes: {'/tab2': (context, state) => const Tab2Page()}, ), ), ]; @override Widget build(BuildContext context) { return Scaffold( body: IndexedStack( index: _currentIndex, children: [ Beamer(routerDelegate: _beamers[0]), Beamer(routerDelegate: _beamers[1]), ], ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentIndex, onTap: (index) { setState(() => _currentIndex = index); _beamers[index].beamToNamed(_beamers[index].initialPath!); }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: 'Tab 1'), BottomNavigationBarItem(icon: Icon(Icons.settings), label: 'Tab 2'), ], ), ); } } // 测试页面 class Tab1Page extends StatelessWidget { const Tab1Page({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Tab 1')), body: Center( child: ElevatedButton( onPressed: () => context.beamToNamed('/details/1'), child: const Text('Go to Details'), ), ), ); } } class Tab2Page extends StatelessWidget { const Tab2Page({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Tab 2')), body: Center(child: const Text('Tab 2 Content')), ); } } class DetailsPage extends StatelessWidget { final String id; const DetailsPage({super.key, required this.id}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Details $id')), body: Center(child: Text('Details Content $id')), ); } }
关键修复点
- 分离路由组:将带底部栏的路由和全屏路由拆分为独立的
BeamLocation,避免路由树混合导致的组件冲突。 - 独立导航状态:每个Tab使用独立的
BeamerDelegate管理导航状态,返回键会优先处理当前Tab内的路由,再回退到上一级。 - 唯一组件Key:给每个
BeamPage分配唯一的ValueKey,帮助Flutter正确识别组件身份,避免焦点管理错误。
内容的提问来源于stack exchange,提问作者andr2k
相关产品推荐
相关产品推荐

