基于go_router与flutter_bloc的持久化BottomNavigationBar的Scaffold架构问询
根据Flutter官方文档,Scaffold不应嵌套使用——它是构建基础视觉结构的核心类,负责AppBar、BottomNavigationBar、Drawer等组件的布局,嵌套会导致Drawer无法覆盖外层BottomNavigationBar等异常问题。
需求目标
- 仅使用一个全局Scaffold
- BottomNavigationBar具备流畅切换动画
- 用
IndexedStack作为Scaffold的body,结合go_router的StatefulShellBranches实现多Navigator架构 - 不同路由页面可自定义对应的AppBar、Drawer及EndDrawer
- 合理使用
flutter_bloc进行状态管理
已尝试的方案及问题
方案一:全局单Scaffold + Bloc管理顶部/侧边栏
架构层级:MaterialApp → ScaffoldBloc → Scaffold → IndexedStack → ShellPage → 业务组件
- 实现逻辑:每个ShellPage初始化时,向ScaffoldBloc发送自身的AppBar、Drawer配置信息,通过BlocBuilder更新全局Scaffold的对应组件
- 存在问题:AppBar切换时会出现异步加载的闪烁问题,嵌套路由的导航逻辑处理复杂且易出问题
方案二:每个路由页面独立Scaffold
架构层级:MaterialApp → IndexedStack → 业务组件 → Scaffold → 实际视图
- 实现逻辑:每个页面自带Scaffold,单独管理自身的AppBar、Drawer等
- 存在问题:每个Scaffold都有独立的BottomNavigationBar,路由切换时动画会被新页面覆盖,出现中断卡顿,用GlobalKey也无法解决
推荐实现方案
核心思路
保留全局唯一Scaffold,通过go_router的路由监听结合flutter_bloc同步当前路由对应的页面配置,同时优化状态更新逻辑避免闪烁,配合StatefulShellBranches实现多Navigator的隔离管理。
具体实现步骤
定义页面配置实体类
封装每个路由页面需要的Scaffold配置项:class PageScaffoldConfig { final PreferredSizeWidget? appBar; final Widget? drawer; final Widget? endDrawer; // 可扩展其他配置,如floatingActionButton等 PageScaffoldConfig({this.appBar, this.drawer, this.endDrawer}); }用Bloc管理全局Scaffold状态
- 定义Bloc事件:
UpdateScaffoldConfig(PageScaffoldConfig config) - 定义Bloc状态:
ScaffoldConfigState(PageScaffoldConfig config) - 优化状态更新:在Bloc中缓存当前配置,只有当新配置与旧配置存在差异时才触发状态变更,避免不必要的重绘
- 定义Bloc事件:
路由监听与配置同步
在页面生命周期中同步配置,同时在页面销毁时恢复上一级配置:class MyPage extends StatefulWidget { const MyPage({super.key}); @override State<MyPage> createState() => _MyPageState(); } class _MyPageState extends State<MyPage> { final _config = PageScaffoldConfig( appBar: AppBar(title: const Text("我的页面")), drawer: const MyDrawer(), ); @override void initState() { super.initState(); context.read<ScaffoldBloc>().add(UpdateScaffoldConfig(_config)); } @override void dispose() { // 从路由栈获取上一页配置(提前在路由extra中存储) final route = GoRouter.of(context).routerDelegate.currentConfiguration.lastOrNull; final previousConfig = route?.extra as PageScaffoldConfig ?? PageScaffoldConfig(); context.read<ScaffoldBloc>().add(UpdateScaffoldConfig(previousConfig)); super.dispose(); } @override Widget build(BuildContext context) { return const Center(child: Text("我的页面")); } }全局Scaffold的构建
固定BottomNavigationBar,通过BlocBuilder动态更新其他组件:class GlobalScaffold extends StatelessWidget { final Widget body; const GlobalScaffold({super.key, required this.body}); @override Widget build(BuildContext context) { return BlocBuilder<ScaffoldBloc, ScaffoldConfigState>( builder: (context, state) { return Scaffold( appBar: state.config.appBar, drawer: state.config.drawer, endDrawer: state.config.endDrawer, bottomNavigationBar: _buildBottomNavigationBar(context), body: body, ); }, ); } Widget _buildBottomNavigationBar(BuildContext context) { final indexNotifier = StatefulShellIndexNotifier.of(context); return BottomNavigationBar( currentIndex: indexNotifier.currentIndex, onTap: (index) => indexNotifier.goBranch(index), items: const [ BottomNavigationBarItem(icon: Icon(Icons.home), label: "首页"), BottomNavigationBarItem(icon: Icon(Icons.person), label: "我的"), ], type: BottomNavigationBarType.fixed, ); } }go_router路由配置
用StatefulShellRoute包裹分支路由,将全局Scaffold作为shell:final GoRouter router = GoRouter( routes: [ StatefulShellRoute.indexedStack( builder: (context, state, navigationShell) { return GlobalScaffold(body: navigationShell); }, branches: [ StatefulShellBranch( routes: [ GoRoute( path: "/home", builder: (context, state) => const HomePage(), routes: [ GoRoute( path: "detail", builder: (context, state) => const HomeDetailPage(), extra: PageScaffoldConfig(appBar: AppBar(title: const Text("详情页"))), ), ], extra: PageScaffoldConfig(appBar: AppBar(title: const Text("首页"))), ), ], ), StatefulShellBranch( routes: [ GoRoute( path: "/profile", builder: (context, state) => const MyPage(), extra: PageScaffoldConfig(appBar: AppBar(title: const Text("我的"))), ), ], ), ], ), ], );
关键优化点
- 避免AppBar闪烁:在Bloc中缓存上一次配置,仅当配置对象变更时更新状态;给AppBar添加
AnimatedSwitcher实现平滑过渡 - 嵌套路由配置恢复:在路由
extra中存储页面配置,出栈时从路由栈读取上一级配置同步到Bloc - BottomNavigationBar动画保持:全局仅一个BottomNavigationBar,用
StatefulShellIndexNotifier.goBranch触发切换,自带平滑动画不会被页面重建中断
内容的提问来源于stack exchange,提问作者Manuel Rauber
相关产品推荐
相关产品推荐

