Flutter+GoRouter+底部导航栏:如何保留标签页子页面导航历史
问题场景
- 基于GoRouter实现了底部导航栏页面切换功能
- Home标签页内需要支持多级跳转:
Home(通用信息流)→ FeedPeople → FeedPeople → ... - 切换至Shop/Discover等其他标签页后切回Home,直接回到Home初始页,丢失之前的FeedPeople导航历史
解决思路与实现步骤
核心是为每个底部标签页维护独立的导航栈,同时记录每个标签的最后访问路径,切换时恢复对应路径的导航状态。
1. 为每个标签页创建独立的NavigatorKey
每个标签页分配专属的GlobalKey<NavigatorState>,确保各自的导航栈独立存储,互不影响。
2. 修改路由配置
将每个底部标签的路由绑定到对应的NavigatorKey,包括其子路由。
3. 记录并恢复标签页的最后访问路径
在底部导航栏组件中维护一个映射表,记录每个标签页最后一次的导航位置,切换标签时直接跳转到该位置而非初始路径。
4. 修正标签页选中状态判断
Home标签的子路由(如/profil/:uuid)需要被识别为Home标签的活跃状态,而非默认的初始路径判断。
修改后的完整代码
import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:my_app/feed.dart'; import 'package:my_app/feed_people.dart'; import 'package:my_app/route_names.dart'; final _rootNavigatorKey = GlobalKey<NavigatorState>(); // 为每个底部标签页创建独立的NavigatorKey final _homeNavigatorKey = GlobalKey<NavigatorState>(); final _discoverNavigatorKey = GlobalKey<NavigatorState>(); final _shopNavigatorKey = GlobalKey<NavigatorState>(); void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( title: 'Flutter Demo', routerConfig: router, ); } final router = GoRouter( initialLocation: '/', navigatorKey: _rootNavigatorKey, routes: [ ShellRoute( navigatorKey: _rootNavigatorKey, pageBuilder: (context, state, child) { print(state.location); return NoTransitionPage( child: ScaffoldWithNavBar( location: state.location, child: child, )); }, routes: [ GoRoute( path: '/', parentNavigatorKey: _homeNavigatorKey, pageBuilder: (context, state) { return const NoTransitionPage( child: Feed(uuid: 'a78987-hiIh!ç767897'), ); }, routes: [ GoRoute( name: RoutesNames.feedPeople, path: 'profil/:uuid', parentNavigatorKey: _homeNavigatorKey, builder: (context, state) => FeedPeople(uuid: state.pathParameters['uuid']!)) ]), GoRoute( path: '/discover', parentNavigatorKey: _discoverNavigatorKey, pageBuilder: (context, state) { return const NoTransitionPage( child: Scaffold( body: Center(child: Text("Discover")), ), ); }, ), GoRoute( parentNavigatorKey: _shopNavigatorKey, path: '/shop', pageBuilder: (context, state) { return const NoTransitionPage( child: Scaffold( body: Center(child: Text("Shop")), ), ); }), ], ), GoRoute( parentNavigatorKey: _rootNavigatorKey, path: '/login', pageBuilder: (context, state) { return NoTransitionPage( key: UniqueKey(), child: Scaffold( appBar: AppBar(), body: const Center( child: Text("Login"), ), ), ); }, ), ], ); } // ignore: must_be_immutable class ScaffoldWithNavBar extends StatefulWidget { String location; ScaffoldWithNavBar({super.key, required this.child, required this.location}); final Widget child; @override State<ScaffoldWithNavBar> createState() => _ScaffoldWithNavBarState(); } class _ScaffoldWithNavBarState extends State<ScaffoldWithNavBar> { int _currentIndex = 0; // 保存每个标签页的最后导航位置 final Map<int, String> _tabLastLocations = { 0: '/', 1: '/discover', 2: '/shop', 3: '/login', }; static const List<MyCustomBottomNavBarItem> tabs = [ MyCustomBottomNavBarItem( icon: Icon(Icons.home), activeIcon: Icon(Icons.home), label: 'HOME', initialLocation: '/', ), MyCustomBottomNavBarItem( icon: Icon(Icons.explore_outlined), activeIcon: Icon(Icons.explore), label: 'DISCOVER', initialLocation: '/discover', ), MyCustomBottomNavBarItem( icon: Icon(Icons.storefront_outlined), activeIcon: Icon(Icons.storefront), label: 'SHOP', initialLocation: '/shop', ), MyCustomBottomNavBarItem( icon: Icon(Icons.account_circle_outlined), activeIcon: Icon(Icons.account_circle), label: 'MY', initialLocation: '/login', ), ]; @override Widget build(BuildContext context) { // 更新当前标签的最后访问位置 _tabLastLocations[_currentIndex] = widget.location; const labelStyle = TextStyle(fontFamily: 'Roboto'); return Scaffold( body: SafeArea(child: widget.child), bottomNavigationBar: BottomNavigationBar( selectedLabelStyle: labelStyle, unselectedLabelStyle: labelStyle, selectedItemColor: const Color(0xFF434343), selectedFontSize: 12, unselectedItemColor: const Color(0xFF838383), showUnselectedLabels: true, type: BottomNavigationBarType.fixed, onTap: (int index) { _goOtherTab(context, index); }, // 修正Home标签的选中判断,识别子路由 currentIndex: widget.location.startsWith('/') && !widget.location.startsWith('/discover') && !widget.location.startsWith('/shop') ? 0 : widget.location == '/discover' ? 1 : widget.location == '/shop' ? 2 : 3, items: tabs, ), ); } void _goOtherTab(BuildContext context, int index) { if (index == _currentIndex) return; GoRouter router = GoRouter.of(context); // 恢复标签页的最后访问位置,无则用初始路径 String location = _tabLastLocations[index] ?? tabs[index].initialLocation; setState(() { _currentIndex = index; }); router.go(location); } } class MyCustomBottomNavBarItem extends BottomNavigationBarItem { final String initialLocation; const MyCustomBottomNavBarItem( {required this.initialLocation, required Widget icon, String? label, Widget? activeIcon}) : super(icon: icon, label: label, activeIcon: activeIcon ?? icon); }
关键说明
- 独立NavigatorKey:每个标签页的导航栈由专属的NavigatorKey管理,切换标签时不会重置其他标签的栈状态
- 路径记录:
_tabLastLocations实时更新每个标签的最后访问路径,确保切换时回到正确的页面 - 子路由识别:通过
startsWith判断Home标签的子路由,保证底部导航栏的选中状态正确
内容的提问来源于stack exchange,提问作者Jérémie Chazelle
相关产品推荐
相关产品推荐

