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

Flutter showModalBottomSheet内部导航点击外部关闭异常问题

问题原因

你当前的实现通过_currentView下标切换弹窗内不同页面,所有页面都处于同一个底部弹窗路由下,没有独立的内部路由栈。你给第二页绑定的WillPopScope会无差别拦截所有作用在这个弹窗路由上的pop请求——不管这个请求来自系统返回键,还是点击弹窗外部遮罩的关闭操作,只要当前在第二页,都会被拦截后切回第一页,这就是点击外部无法直接关弹窗的核心原因。

修复实现

核心要做三件事:

  • 给弹窗内部加独立的嵌套导航栈,把弹窗内的页面跳转和外层弹窗的关闭逻辑隔离开
  • 区分两种pop触发场景:系统返回键触发的pop走「第二页回退第一页,再返回关闭弹窗」的逻辑;点击外部遮罩触发的pop直接关闭整个弹窗,不触发内部页面回退
  • 移除之前用下标切换页面的逻辑,所有弹窗内跳转走嵌套Navigator统一管理

修复后的完整可运行代码如下:

// 调用底部弹窗的入口
void showSortFilterSheet(BuildContext context) {
  showModalBottomSheet(
    context: context,
    isScrollControlled: true,
    backgroundColor: Colors.transparent,
    builder: (sheetContext) {
      return const _BadooStyleSortSheet();
    },
  );
}

class _BadooStyleSortSheet extends StatefulWidget {
  const _BadooStyleSortSheet();

  @override
  State<_BadooStyleSortSheet> createState() => _BadooStyleSortSheetState();
}

class _BadooStyleSortSheetState extends State<_BadooStyleSortSheet> {
  // 弹窗内部独立导航的key,用来操作内部路由栈
  final GlobalKey<NavigatorState> _innerNavKey = GlobalKey<NavigatorState>();
  // 标记当前是否在弹窗内的二级页面
  bool _isOnSecondPage = false;

  @override
  Widget build(BuildContext context) {
    // 最外层包裹WillPopScope,处理系统返回键逻辑
    return WillPopScope(
      onWillPop: () async {
        // 如果当前在二级页面,先回退到一级页面,不关闭弹窗
        if (_isOnSecondPage) {
          _innerNavKey.currentState?.pop();
          return false;
        }
        // 已经在一级页面,直接关闭弹窗
        return true;
      },
      child: Container(
        height: 400,
        width: double.maxFinite,
        decoration: const BoxDecoration(
          color: Colors.white,
          borderRadius: BorderRadius.only(
            topLeft: Radius.circular(60),
            topRight: Radius.circular(60),
          ),
        ),
        // 弹窗内部独立导航栈
        child: Navigator(
          key: _innerNavKey,
          onGenerateRoute: (settings) {
            return MaterialPageRoute(
              builder: (innerContext) {
                // 一级页面
                if (settings.name == '/') {
                  return _FirstPage(
                    onTapGender: () {
                      setState(() {
                        _isOnSecondPage = true;
                      });
                      _innerNavKey.currentState?.pushNamed('/gender');
                    },
                  );
                }
                // 二级性别选择页
                return _SecondPage(
                  onBack: () {
                    setState(() {
                      _isOnSecondPage = false;
                    });
                    _innerNavKey.currentState?.pop();
                  },
                );
              },
            );
          },
          // 监听内部路由变化,同步当前页面状态
          observers: [
            _InnerNavObserver(
              onPageChanged: (isOnSecondPage) {
                WidgetsBinding.instance.addPostFrameCallback((_) {
                  if (mounted) {
                    setState(() {
                      _isOnSecondPage = isOnSecondPage;
                    });
                  }
                });
              },
            )
          ],
        ),
      ),
    );
  }
}

// 内部路由监听,同步当前页面层级
class _InnerNavObserver extends NavigatorObserver {
  final Function(bool isOnSecondPage) onPageChanged;
  _InnerNavObserver({required this.onPageChanged});

  @override
  void didPush(Route route, Route? previousRoute) {
    super.didPush(route, previousRoute);
    onPageChanged(route.settings.name != '/');
  }

  @override
  void didPop(Route route, Route? previousRoute) {
    super.didPop(route, previousRoute);
    onPageChanged(previousRoute?.settings.name != '/');
  }
}

// 一级页面
class _FirstPage extends StatelessWidget {
  final VoidCallback onTapGender;
  const _FirstPage({required this.onTapGender});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text("排序筛选首页"),
          const SizedBox(height: 20),
          ElevatedButton(
            onPressed: onTapGender,
            child: const Text("Show gender"),
          )
        ],
      ),
    );
  }
}

// 二级性别选择页
class _SecondPage extends StatelessWidget {
  final VoidCallback onBack;
  const _SecondPage({required this.onBack});

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          InkWell(
            onTap: onBack,
            child: const Text("返回上一页"),
          ),
          const SizedBox(height: 20),
          const Text("性别选择页")
        ],
      ),
    );
  }
}
关键逻辑说明
  • 嵌套的Navigator只负责管理弹窗内部的页面跳转,外层的底部弹窗路由本身只负责控制弹窗的显示/关闭,两层路由逻辑完全解耦
  • 最外层的WillPopScope只会响应系统返回键的事件:在二级页时先pop内部路由回一级页,在一级页时直接pop外层路由关弹窗
  • 点击外部遮罩触发的pop事件是直接作用在外层底部弹窗路由上的,不会经过内部导航栈和_isOnSecondPage状态的拦截,不管当前在弹窗内哪一页,都会直接关闭整个弹窗,完全符合预期交互

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.26 13:57:13