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
相关产品推荐
相关产品推荐

