Flutter中如何实现带主路由与子路由的流畅页面导航(含底部栏)
解决Flutter底部导航栏下的子路由导航问题(保留底部栏+动态AppBar)
核心方案
给底部导航栏的每个Tab配置独立的子Navigator,将MainPage的Scaffold作为固定容器(承载底部栏和AppBar框架),Tab对应的页面内容由子Navigator渲染。这样Tab内部的页面跳转只会在当前子导航栈中进行,不会替换整个Scaffold,自然保留底部栏,同时可以根据子导航栈的状态动态修改AppBar的内容。
具体实现步骤
1. 改造MainPage,维护子导航键和状态
在MainPage中为每个Tab创建独立的GlobalKey<NavigatorState>,用来控制对应Tab的子导航栈,同时跟踪当前选中的Tab和AppBar的动态配置:
class MainPage extends StatefulWidget { const MainPage({super.key}); @override State<MainPage> createState() => _MainPageState(); } class _MainPageState extends State<MainPage> { int _currentTabIndex = 0; // 为每个Tab创建子导航键 final List<GlobalKey<NavigatorState>> _tabNavKeys = [ GlobalKey<NavigatorState>(), GlobalKey<NavigatorState>(), GlobalKey<NavigatorState>(), ]; // 动态生成AppBar:根据当前Tab的导航栈状态更新 AppBar _buildAppBar() { final currentNavKey = _tabNavKeys[_currentTabIndex]; final hasSubPages = currentNavKey.currentState?.canPop() ?? false; return AppBar( title: Text(_getAppBarTitle()), leading: hasSubPages ? IconButton( icon: const Icon(Icons.arrow_back), onPressed: () => currentNavKey.currentState?.pop(), ) : null, // 根页面不显示返回按钮 ); } String _getAppBarTitle() { final currentNavKey = _tabNavKeys[_currentTabIndex]; // 根据当前Tab和路由栈状态返回标题 switch (_currentTabIndex) { case 0: return '首页'; case 1: return '收藏'; case 2: final currentRoute = currentNavKey.currentState?.context.route; // 如果是城市页面,从路由参数取标题 if (currentRoute?.settings.name == '/state-screen') { return (currentRoute?.settings.arguments as String?) ?? '城市列表'; } return '目的地'; default: return '首页'; } } @override Widget build(BuildContext context) { return Scaffold( appBar: _buildAppBar(), body: Stack( children: List.generate(3, (index) { return Offstage( offstage: index != _currentTabIndex, child: Navigator( key: _tabNavKeys[index], onGenerateRoute: (settings) { // 为每个Tab配置独立路由表 switch (settings.name) { case '/': return MaterialPageRoute( builder: (_) => _getTabPage(index), ); case '/state-screen': // 子页面无需Scaffold,直接返回内容组件 return MaterialPageRoute( builder: (_) => StateScreen(countryName: settings.arguments as String), ); default: return MaterialPageRoute(builder: (_) => const NotFoundPage()); } }, ), ); }), ), bottomNavigationBar: BottomNavigationBar( currentIndex: _currentTabIndex, onTap: (index) { // 切换到当前Tab时,直接返回根页面 if (index == _currentTabIndex) { _tabNavKeys[index].currentState?.popUntil((route) => route.isFirst); } else { setState(() => _currentTabIndex = index); } }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: '首页'), BottomNavigationBarItem(icon: Icon(Icons.favorite), label: '收藏'), BottomNavigationBarItem(icon: Icon(Icons.location_on), label: '目的地'), ], ), ); } Widget _getTabPage(int index) { switch (index) { case 0: return const HomePage(); case 1: return const FavoritesPage(); case 2: return LocationsPage(navigatorKey: _tabNavKeys[2]); default: return const HomePage(); } } }
2. 修改LocationsPage的跳转逻辑
在LocationsPage中,使用当前Tab的子Navigator跳转,而非全局Navigator:
class LocationsPage extends StatelessWidget { final GlobalKey<NavigatorState> navigatorKey; const LocationsPage({super.key, required this.navigatorKey}); @override Widget build(BuildContext context) { return ListView( children: [ ListTile( title: const Text('中国'), onTap: () { navigatorKey.currentState?.pushNamed( '/state-screen', arguments: '中国', ); }, ), // 其他国家列表项... ], ); } }
3. 子页面StateScreen的实现
子页面无需包含Scaffold,仅编写核心内容即可:
class StateScreen extends StatelessWidget { final String countryName; const StateScreen({super.key, required this.countryName}); @override Widget build(BuildContext context) { return ListView( children: [ ListTile(title: Text('${countryName}的城市1')), ListTile(title: Text('${countryName}的城市2')), // 更多城市项... ], ); } }
关键注意点
- 子Navigator路由配置:建议为每个Tab单独维护路由表,避免跨Tab路由冲突;
- AppBar动态更新:通过子导航栈的
canPop()状态判断是否显示返回按钮,标题可通过路由参数或当前路由信息动态获取; - Tab交互逻辑:切换回当前Tab时调用
popUntil回到根页面,和Instagram/Twitter的交互逻辑对齐。
内容的提问来源于stack exchange,提问作者Mihai Carp
相关产品推荐
相关产品推荐

