I am building a Flutter app with a BottomNavigationBar
and a TabBarView
in one of the screens using StatefulShellRoute(go_router
). When I swipe between tabs in the TabBarView and quickly switch to a different bottom navigation item before the tab-switch animation completes, the app crashes with the following error,
setState() or markNeedsBuild() called during build.
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
void main() {
runApp(const App());
}
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) =>
MaterialApp.router(routerConfig: mainRouter);
}
final _mainKey = GlobalKey<NavigatorState>();
final _dashboardKey = GlobalKey<NavigatorState>();
final _favouritesKey = GlobalKey<NavigatorState>();
final _settingsKey = GlobalKey<NavigatorState>();
final GoRouter mainRouter = GoRouter(
initialLocation: '/dashboard',
navigatorKey: _mainKey,
routes: [
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) =>
HomeNavHost(navigationShell: navigationShell),
branches: [
StatefulShellBranch(
navigatorKey: _dashboardKey,
routes: [
GoRoute(
path: '/dashboard',
pageBuilder: (context, state) =>
NoTransitionPage(child: Center(child: Text('Dashboard'))),
),
],
),
StatefulShellBranch(
navigatorKey: _favouritesKey,
routes: [
GoRoute(
path: '/favourites',
pageBuilder: (context, state) =>
NoTransitionPage(child: FavouritesScreen()),
),
],
),
StatefulShellBranch(
navigatorKey: _settingsKey,
routes: [
GoRoute(
path: '/settings',
pageBuilder: (context, state) =>
NoTransitionPage(child: Center(child: Text('Settings'))),
),
],
),
],
)
],
);
class HomeNavHost extends StatelessWidget {
const HomeNavHost({required this.navigationShell, super.key});
final StatefulNavigationShell navigationShell;
@override
Widget build(BuildContext context) {
return Scaffold(
body: navigationShell,
bottomNavigationBar: BottomNavigationBar(
currentIndex: navigationShell.currentIndex,
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
items: [
BottomNavigationBarItem(
icon: Icon(Icons.dashboard),
label: 'Dashboard',
),
BottomNavigationBarItem(
icon: Icon(Icons.favorite),
label: 'Favourites',
),
BottomNavigationBarItem(
icon: Icon(Icons.settings),
label: 'Settings',
),
],
),
);
}
}
class FavouritesScreen extends StatefulWidget {
const FavouritesScreen({super.key});
@override
State<FavouritesScreen> createState() => _FavouritesScreenState();
}
class _FavouritesScreenState extends State<FavouritesScreen> {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Favourites'),
bottom: TabBar(
tabs: [
Tab(text: 'Catergory 1'),
Tab(text: 'Catergory 2'),
],
),
),
body: TabBarView(
children: [
Center(child: Text('Catergory 1')),
Center(child: Text('Catergory 2')),
],
),
),
);
}
}
Using TabBarView
with a BottomNavigationBar
works seamlessly under normal conditions(i.e. without go_router
), and switching tabs by tapping on the tabTitle at the top works without any issues.
However, when using StatefulShellRoute
, swiping between tabs in the TabBarView
and quickly switching to another bottom navigation item results in error.
I've tried:
AutomaticKeepAliveClientMixin
.WidgetsBinding.instance.addPostFrameCallback
& SchedulerBinding.instance.addPostFrameCallback
to delay bottomBar navigation.How can I avoid this issue when switching between BottomNavigationBar
& TabBarView
?
Any guidance or best practices for managing TabBarView
in combination with a BottomNavigationBar
would be greatly appreciated!
Please refactor this part of your dart code:
from:
onTap: (index) => navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
),
to:
onTap: (index) {
WidgetsBinding.instance.addPostFrameCallback((_) {
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
});
},
I tried to reproduce your issue, and I found out that the exception is introduced by the FavouritesScreen
class, where it should be disposed of when the user is not currently viewing this widget in the widget tree. When you swipe or navigate between TabBar
/TabBarView
within the FavouritesScreen
class while navigating through your BottomNavigationBar
, it introduces the
setState() or markNeedsBuild() called during build.
because of these simultaneous events. So you have to refactor your FavouritesScreen
class with this code snippet:
import 'package:flutter/material.dart';
class FavouritesScreen extends StatefulWidget {
const FavouritesScreen({super.key});
@override
State<FavouritesScreen> createState() => _FavouritesScreenState();
}
class _FavouritesScreenState extends State<FavouritesScreen>
with SingleTickerProviderStateMixin {
TabController? _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController!.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Favourites'),
bottom: TabBar(
controller: _tabController,
tabs: [
Tab(text: 'Catergory 1'),
Tab(text: 'Catergory 2'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
Center(child: Text('Catergory 1')),
Center(child: Text('Catergory 2')),
],
),
);
}
}
Then test it again after modifying your code.
It should work as expected now.
I hope it helps!