GoRouter结合StatefulShellRoute时ModalBottomSheet无法正常隐藏的问题
GoRouter StatefulShellRoute下切换标签页时ModalBottomSheet无法自动关闭的解决方案
问题核心
在GoRouter的StatefulShellRoute架构中,将Menu标签页作为独立分支并通过GoRouter push 打开ModalBottomSheet后,切换其他标签页时弹窗无法自动关闭。尝试过全局键、canPop 方法等方案,但存在全局键重复、弹窗关闭不及时等问题。
解决方案思路
- 不要将Menu作为StatefulShellBranch:Menu只是临时弹窗,不属于常驻页面分支,无需加入路由分支列表。
- 直接用原生
showModalBottomSheet打开弹窗:避免依赖GoRouter的导航栈,简化弹窗生命周期管理。 - 监听标签切换事件:切换到非Menu标签时主动关闭弹窗。
- 同步TabController与导航栈索引:处理Menu标签插入后的索引偏移问题,避免状态不一致。
修改后的完整代码
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; final GlobalKey<NavigatorState> _rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root'); final GlobalKey<NavigatorState> _sectionANavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'sectionANav'); void main() { runApp(NestedTabNavigationExampleApp()); } class NestedTabNavigationExampleApp extends StatelessWidget { NestedTabNavigationExampleApp({super.key}); final GoRouter _router = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/a', routes: <RouteBase>[ StatefulShellRoute.indexedStack( builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { return ScaffoldWithNavBar( navigationShell: navigationShell, ); }, branches: <StatefulShellBranch>[ // A分支 StatefulShellBranch( navigatorKey: _sectionANavigatorKey, routes: <RouteBase>[ GoRoute( path: '/a', builder: (BuildContext context, GoRouterState state) => const TabScreen(text: 'A'), ), ], ), // B分支 StatefulShellBranch( routes: <RouteBase>[ GoRoute( path: '/b', builder: (BuildContext context, GoRouterState state) => const TabScreen(text: 'B'), ), ], ), // C分支 StatefulShellBranch( routes: <RouteBase>[ GoRoute( path: '/c', builder: (BuildContext context, GoRouterState state) => const TabScreen(text: 'C'), ), ], ), // D分支 StatefulShellBranch( routes: <RouteBase>[ GoRoute( path: '/d', builder: (BuildContext context, GoRouterState state) => const TabScreen(text: 'D'), ), ], ), ], ), ], ); @override Widget build(BuildContext context) => MaterialApp.router( routerConfig: _router, ); } class TabScreen extends StatelessWidget { const TabScreen({required this.text, super.key}); final String text; @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Text(text, style: const TextStyle(fontSize: 24)), ), ); } } class _MenuBottomSheetBody extends StatelessWidget { const _MenuBottomSheetBody(); @override Widget build(BuildContext context) { return Material( color: Colors.transparent, child: DecoratedBox( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, children: const <Widget>[ ListTile(title: Text('Option 1'), onTap: () {}), ListTile(title: Text('Option 2'), onTap: () {}), ], ), ), ); } } class ScaffoldWithNavBar extends StatefulWidget { const ScaffoldWithNavBar({required this.navigationShell, super.key}); final StatefulNavigationShell navigationShell; @override State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState(); } class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> with TickerProviderStateMixin { late final TabController _tabController; bool _isMenuSheetOpen = false; @override void initState() { super.initState(); // TabBar包含5个标签(A/B/Menu/C/D) _tabController = TabController(length: 5, vsync: this); // 初始同步导航栈索引到TabController(导航栈索引0→A,1→B,2→C,3→D;对应Tab索引0,1,3,4) _tabController.index = widget.navigationShell.currentIndex >=2 ? widget.navigationShell.currentIndex +1 : widget.navigationShell.currentIndex; // 监听Tab切换 _tabController.addListener(_handleTabChange); } void _handleTabChange() { if (_tabController.indexIsChanging) { final tabIndex = _tabController.index; // 切换到非Menu标签时关闭弹窗 if (tabIndex !=2 && _isMenuSheetOpen) { Navigator.of(context).pop(); _isMenuSheetOpen = false; } // 处理导航栈索引偏移:Tab索引3对应导航栈2(C),Tab4对应导航栈3(D) if (tabIndex !=2) { final branchIndex = tabIndex >2 ? tabIndex -1 : tabIndex; widget.navigationShell.goBranch( branchIndex, initialLocation: branchIndex == widget.navigationShell.currentIndex, ); } } } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: widget.navigationShell, bottomNavigationBar: TabBar( controller: _tabController, indicator: TopIndicator(), tabs: const <Tab>[ Tab(icon: Icon(Icons.work), text: 'A'), Tab(icon: Icon(Icons.work), text: 'B'), Tab(icon: Icon(Icons.menu), text: 'Menu'), Tab(icon: Icon(Icons.tab), text: 'C'), Tab(icon: Icon(Icons.settings), text: 'D'), ], onTap: (int index) { if (index ==2) { // 点击Menu标签时打开弹窗 if (!_isMenuSheetOpen) { showModalBottomSheet( context: context, isScrollControlled: true, showDragHandle: true, backgroundColor: Colors.transparent, builder: (context) => const _MenuBottomSheetBody(), ).then((_) { _isMenuSheetOpen = false; // 弹窗关闭后回到之前选中的标签 final prevBranchIndex = widget.navigationShell.currentIndex; _tabController.index = prevBranchIndex >=2 ? prevBranchIndex +1 : prevBranchIndex; }); _isMenuSheetOpen = true; } } else { _tabController.animateTo(index); } }, ), ); } } class TopIndicator extends Decoration { @override BoxPainter createBoxPainter([VoidCallback? onChanged]) => _TopIndicatorBox(); } class _TopIndicatorBox extends BoxPainter { @override void paint(Canvas canvas, Offset offset, ImageConfiguration cfg) { final paint = Paint() ..color = Colors.black ..strokeWidth =2 ..isAntiAlias = true ..strokeCap = StrokeCap.square; canvas.drawLine( Offset(offset.dx, 0.5), Offset(cfg.size!.width + offset.dx, 0.5), paint, ); } }
关键修改点说明
- 移除Menu路由分支:将原来的Menu分支从
StatefulShellBranch列表中删除,避免弹窗绑定到特定导航栈。 - 原生弹窗替代GoRouter页面:用
showModalBottomSheet直接打开弹窗,无需自定义BottomSheetPage。 - 标签切换监听:在
_handleTabChange中检测到切换到非Menu标签时,主动关闭弹窗。 - 索引偏移处理:因为Menu标签插入在B和C之间,导航栈索引与Tab索引存在偏移,切换时需要做对应转换。
- 弹窗状态跟踪:用
_isMenuSheetOpen变量避免重复打开弹窗,弹窗关闭后自动恢复到之前的标签页。
内容的提问来源于stack exchange,提问作者Kyoto
相关产品推荐
相关产品推荐

