Flutter GoRouter+ShellRoute+BottomNavigationBar返回逻辑异常问题
用GoRouter ShellRoute实现Instagram式返回逻辑的解决办法
1. 别用拦截器,自己搞个栈管理
back_button_interceptor容易和ShellRoute的导航栈冲突,不如自己写个简单的栈管理类,给每个底部导航页单独维护访问历史:
class PageStackManager { // 给每个底部路由分配一个栈 final _stacks = <String, List<String>>{ '/': [], '/search': [], '/profile': [], }; String? _activeRoute; // 切换底部导航时,标记当前活跃的路由 void setActiveRoute(String route) => _activeRoute = route; // 跳转新页面时,把当前路径压入活跃路由的栈 void push(String currentPath) { if (_activeRoute != null) { _stacks[_activeRoute!]?.add(currentPath); } } // 返回时,从活跃路由的栈弹出上一个路径 String? pop() { if (_activeRoute == null) return null; final stack = _stacks[_activeRoute!]; if (stack?.isNotEmpty == true) { return stack?.removeLast(); } // 当前栈空了,找其他底部路由的栈有没有历史记录 for (final route in _stacks.keys.where((k) => k != _activeRoute)) { if (_stacks[route]?.isNotEmpty == true) { return _stacks[route]?.last; } } return null; } } // 全局实例,方便各处调用 final pageStackManager = PageStackManager();
2. 用PopScope处理返回逻辑(别放Expanded里)
把PopScope放在Scaffold的外面,确保它能拿到正确的导航上下文,避免被Expanded的上下文干扰:
class ScaffoldWithNavBar extends StatelessWidget { const ScaffoldWithNavBar({super.key, required this.child}); final Widget child; @override Widget build(BuildContext context) { return PopScope( // 禁止默认的pop行为,自己处理返回逻辑 canPop: false, onPopInvoked: (didPop) async { if (didPop) return; final nextRoute = pageStackManager.pop(); if (nextRoute != null) { // 用go跳转而不是pop,避免打乱Shell的根栈 context.go(nextRoute); } else { // 所有栈都空了,退出APP SystemNavigator.pop(); } }, child: Scaffold( bottomNavigationBar: BottomNavigationBar( items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'), BottomNavigationBarItem(icon: Icon(Icons.search), label: '搜索'), BottomNavigationBarItem(icon: Icon(Icons.person), label: '我的'), ], currentIndex: _getCurrentTabIndex(context), onTap: (index) { final targetRoute = _getRouteFromIndex(index); final currentPath = GoRouterState.of(context).uri.toString(); // 切换底部tab时,把当前路径存入栈 pageStackManager.push(currentPath); pageStackManager.setActiveRoute(targetRoute); context.go(targetRoute); }, ), // 这里放心用Expanded,因为PopScope在外面 body: Column( children: [ Expanded(child: child), ], ), ), ); } int _getCurrentTabIndex(BuildContext context) { final path = GoRouterState.of(context).uri.toString(); if (path.startsWith('/search')) return 1; if (path.startsWith('/profile')) return 2; return 0; } String _getRouteFromIndex(int index) { switch (index) { case 1: return '/search'; case 2: return '/profile'; default: return '/'; } } }
3. 修复根路径返回问题
当在根路径/且栈为空时,点击返回要先检查其他底部tab有没有历史记录,有的话跳过去,没有再退出:
onPopInvoked: (didPop) async { if (didPop) return; final currentPath = GoRouterState.of(context).uri.toString(); final nextRoute = pageStackManager.pop(); if (nextRoute != null) { context.go(nextRoute); } else { if (currentPath == '/') { // 根路径且无历史,直接退出 SystemNavigator.pop(); } else { // 其他路径无历史,跳回根路径 context.go('/'); } } },
4. GoRouter配置要对应上
确保ShellRoute把所有底部导航页都包含进去,还要加个Observer自动监听路由变化:
final router = GoRouter( observers: [RouteStackObserver()], routes: [ ShellRoute( builder: (context, state, child) => ScaffoldWithNavBar(child: child), routes: [ GoRoute( path: '/', builder: (context, state) => const HomePage(), ), GoRoute( path: '/search', builder: (context, state) => const SearchPage(), ), GoRoute( path: '/profile', builder: (context, state) => const ProfilePage(), ), ], ), ], ); // 自动监听路由跳转,把路径压入栈 class RouteStackObserver extends NavigatorObserver { @override void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) { super.didPush(route, previousRoute); if (route.settings.name != null) { pageStackManager.push(route.settings.name!); } } }
为什么加Expanded会失效?
Expanded是Column的子组件,会创建新的上下文,导致内部的导航监听无法关联到GoRouter的顶层Navigator。把PopScope放在Scaffold外面,就能确保监听的是正确的导航上下文,Expanded只管布局就行。
内容的提问来源于stack exchange,提问作者Filip Brablec
相关产品推荐
相关产品推荐

