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

GoRouter结合StatefulShellRoute时ModalBottomSheet无法正常隐藏的问题

GoRouter StatefulShellRoute下切换标签页时ModalBottomSheet无法自动关闭的解决方案

问题核心

在GoRouter的StatefulShellRoute架构中,将Menu标签页作为独立分支并通过GoRouter push 打开ModalBottomSheet后,切换其他标签页时弹窗无法自动关闭。尝试过全局键、canPop 方法等方案,但存在全局键重复、弹窗关闭不及时等问题。

解决方案思路

  1. 不要将Menu作为StatefulShellBranch:Menu只是临时弹窗,不属于常驻页面分支,无需加入路由分支列表。
  2. 直接用原生showModalBottomSheet打开弹窗:避免依赖GoRouter的导航栈,简化弹窗生命周期管理。
  3. 监听标签切换事件:切换到非Menu标签时主动关闭弹窗。
  4. 同步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,
    );
  }
}

关键修改点说明

  1. 移除Menu路由分支:将原来的Menu分支从StatefulShellBranch列表中删除,避免弹窗绑定到特定导航栈。
  2. 原生弹窗替代GoRouter页面:用showModalBottomSheet直接打开弹窗,无需自定义BottomSheetPage。
  3. 标签切换监听:在_handleTabChange中检测到切换到非Menu标签时,主动关闭弹窗。
  4. 索引偏移处理:因为Menu标签插入在B和C之间,导航栈索引与Tab索引存在偏移,切换时需要做对应转换。
  5. 弹窗状态跟踪:用_isMenuSheetOpen变量避免重复打开弹窗,弹窗关闭后自动恢复到之前的标签页。

内容的提问来源于stack exchange,提问作者Kyoto

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.18 02:52:33