Flutter中结合GoRouter与TabBarView实现带状态保持的底部导航
针对你需要的「底部NavigationBar切换3个页面+TabBarView保持页面状态+子页面隐藏导航栏」的需求,StatefulShellRoute是GoRouter官方推荐的标准方案,完美适配嵌套导航、状态保持和子页面独立导航的场景,以下是完整的规范实现:
一、核心实现思路
- 用
StatefulShellRoute.indexedStack作为根路由,自动维护每个tab页面的状态栈 - 每个tab对应一个
StatefulShellBranch,分支内定义页面路由 - 子页面通过
parentNavigatorKey绑定根导航,打开时自动隐藏底部NavigationBar - 基于
AutomaticKeepAliveClientMixin封装页面基类,统一实现状态保持 - 主页面中双向同步
TabController与StatefulNavigationShell,确保路由与UI状态一致
二、完整代码实现
1. 全局导航Key定义
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; final _rootNavigatorKey = GlobalKey<NavigatorState>(); final _shellNavigatorKey = GlobalKey<NavigatorState>();
2. GoRouter配置
final router = GoRouter( navigatorKey: _rootNavigatorKey, initialLocation: '/', routes: [ StatefulShellRoute.indexedStack( navigatorKey: _shellNavigatorKey, builder: (context, state, navigationShell) { // 主页面承载NavigationBar和TabBarView return MainPage(navigationShell: navigationShell); }, branches: [ // 首页分支 StatefulShellBranch( routes: [ GoRoute( path: '/', name: 'home', builder: (context, state) => const HomePage(), ), ], ), // 设置页分支(含子页面) StatefulShellBranch( routes: [ GoRoute( path: '/settings', name: 'settings', builder: (context, state) => const SettingsPage(), routes: [ GoRoute( path: 'edit', name: 'settings-edit', // 绑定根导航,子页面不显示底部导航栏 parentNavigatorKey: _rootNavigatorKey, builder: (context, state) => const SettingsEditPage(), ), ], ), ], ), // 日历页分支 StatefulShellBranch( routes: [ GoRoute( path: '/calendar', name: 'calendar', builder: (context, state) => const CalendarPage(), ), ], ), ], ), ], );
3. 主页面(MainPage)实现
class MainPage extends StatefulWidget { final StatefulNavigationShell navigationShell; const MainPage({super.key, required this.navigationShell}); @override State<MainPage> createState() => _MainPageState(); } class _MainPageState extends State<MainPage> with TickerProviderStateMixin { late final TabController _tabController; @override void initState() { super.initState(); _tabController = TabController( length: widget.navigationShell.branches.length, vsync: this, initialIndex: widget.navigationShell.currentIndex, ); // TabController切换时同步到路由栈 _tabController.addListener(() { if (_tabController.indexIsChanging) { widget.navigationShell.goBranch( _tabController.index, initialLocation: _tabController.index != widget.navigationShell.currentIndex, ); } }); // 路由栈变化时同步到TabController widget.navigationShell.addListener(() { if (_tabController.index != widget.navigationShell.currentIndex) { _tabController.animateTo(widget.navigationShell.currentIndex); } }); } @override void dispose() { _tabController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( body: TabBarView( controller: _tabController, physics: const NeverScrollableScrollPhysics(), // 禁用滑动切换(可选) children: List.generate( widget.navigationShell.branches.length, (index) => widget.navigationShell.branches[index].routes.first.builder(context, GoRouterState.empty), ), ), bottomNavigationBar: NavigationBar( selectedIndex: widget.navigationShell.currentIndex, onDestinationSelected: (index) { widget.navigationShell.goBranch( index, initialLocation: index != widget.navigationShell.currentIndex, ); }, destinations: const [ NavigationDestination(icon: Icon(Icons.home), label: '首页'), NavigationDestination(icon: Icon(Icons.settings), label: '设置'), NavigationDestination(icon: Icon(Icons.calendar_today), label: '日历'), ], ), ); } }
4. 状态保持页面基类
abstract class BaseKeepAlivePage extends StatefulWidget { const BaseKeepAlivePage({super.key}); @override BaseKeepAlivePageState createState() => BaseKeepAlivePageState(); } class BaseKeepAlivePageState extends State<BaseKeepAlivePage> with AutomaticKeepAliveClientMixin { @override bool get wantKeepAlive => true; @override Widget build(BuildContext context) { super.build(context); // 必须调用,否则状态保持失效 return buildPage(context); } // 子类实现页面内容 Widget buildPage(BuildContext context); }
5. 业务页面实现
// 首页 class HomePage extends BaseKeepAlivePage { const HomePage({super.key}); @override Widget buildPage(BuildContext context) { return const Center(child: Text('首页')); } } // 设置页 class SettingsPage extends BaseKeepAlivePage { const SettingsPage({super.key}); @override Widget buildPage(BuildContext context) { return Center( child: ElevatedButton( onPressed: () => context.goNamed('settings-edit'), child: const Text('进入编辑页面'), ), ); } } // 设置编辑页(子页面) class SettingsEditPage extends StatelessWidget { const SettingsEditPage({super.key}); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('编辑设置')), body: const Center(child: Text('编辑页面')), ); } } // 日历页 class CalendarPage extends BaseKeepAlivePage { const CalendarPage({super.key}); @override Widget buildPage(BuildContext context) { return const Center(child: Text('日历页')); } }
三、关键细节说明
- StatefulShellRoute.indexedStack:内部维护一个IndexedStack,自动保存每个tab页面的状态,配合
AutomaticKeepAliveClientMixin实现页面状态持久化 - parentNavigatorKey:子页面绑定根导航栈后,打开时会覆盖整个屏幕,底部NavigationBar自动隐藏,返回时回到父页面
- 双向状态同步:确保通过NavigationBar点击、路由跳转(如
context.goNamed)都能同步更新TabBarView的选中状态 - 禁用滑动切换:通过
NeverScrollableScrollPhysics禁止TabBarView滑动,避免和NavigationBar的交互冲突,若需要滑动可移除该配置
内容的提问来源于stack exchange,提问作者Hùng Việt
相关产品推荐
相关产品推荐

