Flutter:GoogleBottomNavigationBar结合go_router使用报错及最佳实践咨询
首次在Flutter项目中尝试结合使用GoogleBottomNavigationBar(即GNav)与go_router包,切换底部导航栏标签时触发如下错误,寻求符合最佳实践的正确使用方式:
报错详情
[VERBOSE-2:dart_vm_initializer.cc(41)] Unhandled Exception: setState() called after dispose(): _GNavState#d598b(lifecycle state: defunct, not mounted) This error happens if you call setState() on a State object for a widget that no longer appears in the widget tree (e.g., whose parent widget no longer includes the widget in its build). This error can occur when code calls setState() from a timer or an animation callback. The preferred solution is to cancel the timer or stop listening to the animation in the dispose() callback. Another solution is to check the "mounted" property of this object before calling setState() to ensure the object is still in the tree. This error might indicate a memory leak if setState() is being called because another object is retaining a reference to this State object after it has been removed from the tree. To avoid memory leaks, consider breaking the reference to this object during dispose(). #0 State.setState.<anonymous closure> (package:flutter/src/widgets/framew<…>
页面示例
| 当前页面 | 首页 |
|---|---|
![]() | ![]() |
相关代码
main.dart
void main() => runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => RootProvider()), ], child: const MyApp(), ), ); class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp.router( debugShowCheckedModeBanner: false, theme: CustomThemeData().lightTheme, darkTheme: CustomThemeData().darkTheme, themeMode: Provider.of<RootProvider>(context).isNight ? ThemeMode.dark : ThemeMode.light, routerConfig: NavigatorRoutes().router, ); } } class Root extends StatefulWidget { const Root({super.key}); @override State<Root> createState() => _RootState(); } class _RootState extends State<Root> with SingleTickerProviderStateMixin { late final AnimationController _animationController; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, ); } @override void dispose() { _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { int selectedIndexOfBottomNavigationBar = Provider.of<RootProvider>(context).selectedIndexOfBottomNavigationBar; bool isNight = Provider.of<RootProvider>(context).isNight; return Scaffold( appBar: AppBar( title: InkWell( child: const Text("go sample"), onTap: () => NavigatorManager.instance.go( context, route: NavigateRoutes.sample.withParaph, ), ), actions: [ SizedBox( width: 90, height: 80, child: _changeThemeToggleButton(context, isNight), ).onlyPadding(right: PaddingManager.commonHorizontal), ], ), body: Center( child: NavigatorRoutes().navPages.elementAt(selectedIndexOfBottomNavigationBar), ).symmetricpadding( horizontal: PaddingManager.commonHorizontal, vertical: PaddingManager.commonVertical, ), bottomNavigationBar: CustomBottomNavigationBar( selectedIndexOfBottomNavigationBar: selectedIndexOfBottomNavigationBar, ).onlyPadding( left: PaddingManager.horizontalOfBottomNavigationBar, right: PaddingManager.horizontalOfBottomNavigationBar, bottom: PaddingManager.onlyBottomOfBottomNavigationBar, top: PaddingManager.onlyTopOfBottomNavigationBar, ), ); } InkWell _changeThemeToggleButton(BuildContext context, bool isNight) { return InkWell( onTap: () { Provider.of<RootProvider>(context, listen: false).changeIsNight(); debugPrint(isNight.toString()); if (!isNight) { _animationController.animateTo(0.5); } else { _animationController.animateTo(-0.5); } }, child: Lottie.asset( LottieUrls.pathOfToggleThemeButton, fit: BoxFit.cover, controller: _animationController, onLoaded: (composition) { _animationController ..duration = composition.duration ..value = 0.5; }, ), ); } }
navigator_routes.dart
/// 封装Flutter应用的导航路由和页面 final class NavigatorRoutes { NavigatorRoutes() : _router = GoRouter( initialLocation: NavigateRoutes.root.justParaph, routes: _items, errorBuilder: (context, state) => const Scaffold( body: Center( child: Text('页面未找到'), ), ), ); final GoRouter _router; /// 定义导航路由及其对应的页面构建器 static final _items = [ GoRoute( path: NavigateRoutes.root.justParaph, name: NavigateRoutes.root.name, builder: (context, state) => const Root(), routes: [ GoRoute( path: NavigateRoutes.sample.name, builder: (context, state) => const Sample(), ), ], ), ]; static const List<Widget> _navPages = [ Home(), SearchPlaces(), CurrentPlace(), ]; GoRouter get router => _router; List<Widget> get navPages => _navPages; }
navigator_manager.dart
final class NavigatorManager { NavigatorManager._(); static NavigatorManager instance = NavigatorManager._(); void go(BuildContext context, {required String route, Object? extras}) { // GoRouter.of(context).goNamed(route); context.go(route); } }
custom_bottom_navigation_bar.dart
/// 自定义底部导航栏组件,包含图标和文本 class CustomBottomNavigationBar extends StatelessWidget { const CustomBottomNavigationBar({ super.key, required this.selectedIndexOfBottomNavigationBar, }); final int selectedIndexOfBottomNavigationBar; @override Widget build(BuildContext context) { return Container( decoration: BoxDecoration( color: Theme.of(context).bottomNavigationBarTheme.backgroundColor ?? ColorManager.darkSecondaryColor, borderRadius: BorderRadiusManager.commonAllBorderRadius, ), child: GNav( activeColor: Theme.of(context).bottomNavigationBarTheme.selectedItemColor ?? ColorManager.activeColor, gap: 8, selectedIndex: selectedIndexOfBottomNavigationBar, onTabChange: (index) { Provider.of<RootProvider>(context, listen: false).selectedIndexOfBottomNavigationBar = index; }, tabs: const [ GButton( icon: FontAwesomeIcons.house, text: TextManager.textHome, ), GButton( icon: FontAwesomeIcons.magnifyingGlass, text: TextManager.textSearch, ), GButton( icon: FontAwesomeIcons.locationCrosshairs, text: TextManager.textCurrent, ), ], ), ); } }
root_provider.dart
class RootProvider extends ChangeNotifier { int _selectedIndexOfBottomNavigationBar = 0; bool _isNight = true; int get selectedIndexOfBottomNavigationBar => _selectedIndexOfBottomNavigationBar; bool get isNight => _isNight; set selectedIndexOfBottomNavigationBar(int value) { _selectedIndexOfBottomNavigationBar = value; notifyListeners(); } void changeIsNight() { _isNight = !_isNight; notifyListeners(); } }
解决方案
问题根源
- 路由与底部导航逻辑脱节:当前直接通过替换
body的Widget切换页面,未结合go_router的路由机制,导致GNav状态与页面生命周期不一致 - 动画调用未校验挂载状态:切换主题时的动画操作未检查页面是否已销毁,触发disposed后的
setState调用 - 路由实例重复创建:每次访问
NavigatorRoutes()都会生成新实例,导致路由状态混乱
修复步骤与最佳实践
1. 重构路由结构,用ShellRoute绑定底部导航容器
将底部导航的每个页面注册为go_router的子路由,通过ShellRoute将Root作为容器,统一管理底部导航:
修改navigator_routes.dart:
final class NavigatorRoutes { NavigatorRoutes._(); static final instance = NavigatorRoutes._(); final GoRouter _router = GoRouter( initialLocation: NavigateRoutes.home.justParaph, routes: _items, errorBuilder: (context, state) => const Scaffold( body: Center( child: Text('页面未找到'), ), ), ); static final _items = [ ShellRoute( builder: (context, state, child) => const Root(), routes: [ GoRoute( path: NavigateRoutes.home.justParaph, name: NavigateRoutes.home.name, builder: (context, state) => const Home(), ), GoRoute( path: NavigateRoutes.search.justParaph, name: NavigateRoutes.search.name, builder: (context, state) => const SearchPlaces(), ), GoRoute( path: NavigateRoutes.current.justParaph, name: NavigateRoutes.current.name, builder: (context, state) => const CurrentPlace(), ), GoRoute( path: NavigateRoutes.sample.justParaph, name: NavigateRoutes.sample.name, builder: (context, state) => const Sample(), ), ], ), ]; GoRouter get router => _router; }
2. 修改Root页面,通过RouterOutlet展示路由页面
Root作为容器,不再手动切换body内容,直接用RouterOutlet展示当前路由对应的页面:
修改main.dart中_RootState的build方法:
@override Widget build(BuildContext context) { bool isNight = Provider.of<RootProvider>(context).isNight; // 从路由状态获取当前选中索引 final location = GoRouterState.of(context).location; int selectedIndex = 0; if (location == NavigateRoutes.search.justParaph) selectedIndex = 1; else if (location == NavigateRoutes.current.justParaph) selectedIndex = 2; return Scaffold( appBar: AppBar( title: InkWell( child: const Text("go sample"), onTap: () => NavigatorManager.instance.go( context, route: NavigateRoutes.sample.justParaph, ), ), actions: [ SizedBox( width: 90, height: 80, child: _changeThemeToggleButton(context, isNight), ).onlyPadding(right: PaddingManager.commonHorizontal), ], ), body: const RouterOutlet(), bottomNavigationBar: CustomBottomNavigationBar( selectedIndexOfBottomNavigationBar: selectedIndex, ).onlyPadding( left: PaddingManager.horizontalOfBottomNavigationBar, right: PaddingManager.horizontalOfBottomNavigationBar, bottom: PaddingManager.onlyBottomOfBottomNavigationBar, top: PaddingManager.onlyTopOfBottomNavigationBar, ), ); }
3. 调整底部导航切换逻辑,通过路由跳转实现页面切换
修改custom_bottom_navigation_bar.dart的onTabChange:
onTabChange: (index) { switch(index) { case 0: context.goNamed(NavigateRoutes.home.name); break; case 1: context.goNamed(NavigateRoutes.search.name); break; case 2: context.goNamed(NavigateRoutes.current.name); break; } },
此时可移除RootProvider中selectedIndexOfBottomNavigationBar相关代码,直接通过路由状态维护选中状态。
4. 修复动画控制器的挂载校验问题
在动画调用前添加mounted判断,避免在页面销毁后执行:
InkWell _changeThemeToggleButton(BuildContext context, bool isNight) { return InkWell( onTap: () { final provider = Provider.of<RootProvider>(context, listen: false); provider.changeIsNight(); debugPrint(provider.isNight.toString()); if (!mounted) return; if (!isNight) { _animationController.animateTo(0.5); } else { _animationController.animateTo(0.0); } }, child: Lottie.asset( LottieUrls.pathOfToggleThemeButton, fit: BoxFit.cover, controller: _animationController, onLoaded: (composition) { if (!mounted) return; _animationController ..duration = composition.duration ..value = 0.5; }, ), ); }
5. 全局使用单例路由实例
修改main.dart中的路由配置:
routerConfig: NavigatorRoutes.instance.router,
最终效果
- 底部导航与go_router路由完全绑定,符合路由管理最佳实践
- 动画操作前校验页面挂载状态,彻底解决
setState() called after dispose错误 - 路由状态与底部导航选中状态自动同步,无生命周期不一致问题
内容的提问来源于stack exchange,提问作者TheSylar
相关产品推荐
相关产品推荐



