I am using a BottomNavigationBar together with a TabController. By clicking on the different Tabs of the BottomNavigationBar the TabView is changing the content. However if I swipe on the TabView to switch to another view/tab the BottomNavigationBar is not updating to the tab I swiped to. I already have added a listener to the TabController to detect changes. But how can I update BottomNavigationBar programmatically to reflect the change?
4 Answers
I think it is way more elegant to use PageView
instead of TabBarView
specially in your case.
class BottomBarExample extends StatefulWidget {
@override
_BottomBarExampleState createState() => new _BottomBarExampleState();
}
class _BottomBarExampleState extends State<BottomBarExample> {
int _page = 0;
PageController _c;
@override
void initState(){
_c = new PageController(
initialPage: _page,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return new Scaffold(
bottomNavigationBar: new BottomNavigationBar(
currentIndex: _page,
onTap: (index){
this._c.animateToPage(index,duration: const Duration(milliseconds: 500),curve: Curves.easeInOut);
},
items: <BottomNavigationBarItem>[
new BottomNavigationBarItem(icon: new Icon(Icons.supervised_user_circle), title: new Text("Users")),
new BottomNavigationBarItem(icon: new Icon(Icons.notifications), title: new Text("Alerts")),
new BottomNavigationBarItem(icon: new Icon(Icons.email), title: new Text("Inbox")),
],
),
body: new PageView(
controller: _c,
onPageChanged: (newPage){
setState((){
this._page=newPage;
});
},
children: <Widget>[
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.supervised_user_circle),
new Text("Users")
],
),
),
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.notifications),
new Text("Alerts")
],
),
),
new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Icon(Icons.mail),
new Text("Inbox")
],
),
),
],
),
);
}
}

- 50,824
- 20
- 115
- 113
-
1Great solution! – Daniel Mar 21 '20 at 16:29
you should update the current index of bottomNavigationBar to match the new index of tabview when you swipe on the TabView
class _TabBarWithBottomNavBarState extends State<TabBarWithBottomNavBar> with SingleTickerProviderStateMixin {
int _bottomNavBarIndex = 0;
TabController _tabController;
void _tabControllerListener(){
setState(() {
_bottomNavBarIndex = _tabController.index;
});
}
@override
void initState() {
_tabController = TabController(length: 3, vsync: this);
_tabController.addListener(_tabControllerListener);
super.initState();
}
@override
void dispose() {
_tabController.dispose();
_tabController.removeListener(_tabControllerListener);
super.dispose();
}

- 8,815
- 5
- 24
- 42
Another perspective about the BottomNavigationBar
The way many people and even the official documentation is facing the extra navigation is not like on the Web Navigation. They are not rendering a common and/or a partially common template with available links to be executed after a click. Instead, what they are implementing is a preload of different screens in an additional "tabs_screen" (basically just an additional Scaffold that wraps and renders one screen at the time) and then, depending of the given index on the onTap even of the BottomNavigationBar, you will use that index to determine which screen will be actually rendered inside that scaffold. That's all.
Instead, I recommend to handle the Extra Navigation this way:
I added the BottomNavigationBar as usual, but instead of preloading/instantiating the screens on a List _pages, what I did was simply adding another named route for the FavoritesScreen,
I design my bottomNavigationBar:
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => onTapSelectNavigation(index, context),
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.category,
),
label: '',
activeIcon: Icon(
Icons.category,
color: Colors.red,
),
tooltip: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(
Icons.star,
),
label: '',
activeIcon: Icon(
Icons.star,
color: Colors.red,
),
tooltip: 'Favorites',
),
],
currentIndex: _activeIndex,
),
And then on the onTap even I call the onTapSelectNavigation method.
In that method, based on the given clicked tab index, I will decide which route I will call. This BottomNavigationBar is available for the whole app. Why? because I'm implementing this on my FeeddyScaffold, which is common for absolutely all the screens, because FeeddyScaffold wraps all the inner widgets on all of those screens. On each screen I instantiate FeeddyScaffold passing a list of Widgets as a named parameter. This way I guarantee that the scaffold will be common for every single screens, and if I implement the Extra Navigation of that common scaffold, then it will be available for the screens. This is my FeeddyScaffold component:
// Packages:
import 'package:feeddy_flutter/_inner_packages.dart';
import 'package:feeddy_flutter/_external_packages.dart';
// Screens:
import 'package:feeddy_flutter/screens/_screens.dart';
// Models:
import 'package:feeddy_flutter/models/_models.dart';
// Components:
import 'package:feeddy_flutter/components/_components.dart';
// Helpers:
import 'package:feeddy_flutter/helpers/_helpers.dart';
// Utilities:
import 'package:feeddy_flutter/utilities/_utilities.dart';
class FeeddyScaffold extends StatefulWidget {
// Properties:
final bool _showPortraitOnly = false;
final String appTitle;
final Function onPressedAdd;
final String objectName;
final int objectsLength;
final List<Widget> innerWidgets;
final int activeIndex;
// Constructor:
const FeeddyScaffold({
Key key,
this.appTitle,
this.onPressedAdd,
this.objectName,
this.objectsLength,
this.innerWidgets,
this.activeIndex,
}) : super(key: key);
@override
_FeeddyScaffoldState createState() => _FeeddyScaffoldState();
}
class _FeeddyScaffoldState extends State<FeeddyScaffold> {
final bool _showPortraitOnly = false;
int _activeIndex;
@override
void initState() {
// TODO: implement initState
super.initState();
_activeIndex = widget.activeIndex;
}
void onTapSelectNavigation(int index, BuildContext context) {
switch (index) {
case 0:
Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
break;
case 1:
Navigator.pushNamed(context, FavoritesScreen.screenId);
break;
}
}
@override
Widget build(BuildContext context) {
AppData appData = Provider.of<AppData>(context, listen: true);
Function closeAllThePanels = appData.closeAllThePanels; // Drawer related:
bool deviceIsIOS = DeviceHelper.deviceIsIOS(context);
// WidgetsFlutterBinding.ensureInitialized(); // Without this it might not work in some devices:
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
// DeviceOrientation.portraitDown,
if (!_showPortraitOnly) ...[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
]);
FeeddyAppBar appBar = FeeddyAppBar(
appTitle: widget.appTitle,
onPressedAdd: widget.onPressedAdd,
objectName: widget.objectName,
);
return Scaffold(
appBar: appBar,
onDrawerChanged: (isOpened) {
if (!isOpened) {
closeAllThePanels();
}
},
drawer: FeeddyDrawer(),
body: NativeDeviceOrientationReader(
builder: (context) {
final orientation = NativeDeviceOrientationReader.orientation(context);
bool safeAreaLeft = DeviceHelper.isLandscapeLeft(orientation);
bool safeAreaRight = DeviceHelper.isLandscapeRight(orientation);
bool isLandscape = DeviceHelper.isLandscape(orientation);
return SafeArea(
left: safeAreaLeft,
right: safeAreaRight,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widget.innerWidgets,
),
);
},
),
bottomNavigationBar: BottomNavigationBar(
onTap: (index) => onTapSelectNavigation(index, context),
items: [
BottomNavigationBarItem(
icon: Icon(
Icons.category,
),
label: '',
activeIcon: Icon(
Icons.category,
color: Colors.red,
),
tooltip: 'Categories',
),
BottomNavigationBarItem(
icon: Icon(
Icons.star,
),
label: '',
activeIcon: Icon(
Icons.star,
color: Colors.red,
),
tooltip: 'Favorites',
),
],
currentIndex: _activeIndex,
),
// FAB
floatingActionButton: deviceIsIOS
? null
: FloatingActionButton(
tooltip: 'Add ${widget.objectName.inCaps}',
child: Icon(Icons.add),
onPressed: () => widget.onPressedAdd,
),
// floatingActionButtonLocation: FloatingActionButtonLocation.endDocked,
floatingActionButtonLocation: deviceIsIOS ? null : FloatingActionButtonLocation.endDocked,
);
}
}
So, on the onTapSelectNavigation method, depending on the given tab index, and with a simple Switch case sentence, I decide which named route to call. As simple as that.
void onTapSelectNavigation(int index, BuildContext context) {
switch (index) {
case 0:
Navigator.pushNamed(context, FoodCategoryIndexScreen.screenId);
break;
case 1:
Navigator.pushNamed(context, FavoritesScreen.screenId);
break;
}
}
Now, this is not enough. Why? Because when I make available the Extra Navigation to the whole app, then happens something curious.
If we just use a simple setState to set and store locally in our local state the activeTab on this common scaffold, it will work as a charm when we travel between both tabs. However, when from the FoodCategoryIndexScreen we click into a specific FoodCategory, accessing the FoodCategoryShowScreen that holds the FoodRecipesList (meals) and/or into a specific meal, and then we swipe back on IOS or click back on both platforms, the active tab feature gets crazy. It will no longer show properly the current active tab.
Why?
Because you traveled there by popping the routes, not by clicking and then executing the onTab event, and therefore the setState function won't be executed and therefore the activeTab won't be updated.
The solution:
Using the RouteObserver capabilities. This can be done manually, but the simplest way is just to install the route_observer_mixin package. And this is what we are gonna do next:
- In your main file you will wrap your whole app with the RouteObserverProvider, included in the mentioned mixin. (I just added now to upper one [RouteObserverProvider], there rest were already added by me before):
void main() {
runApp(MultiProvider(
providers: [
RouteObserverProvider(),
// Config about the app:
ChangeNotifierProvider<AppData>(
create: (context) => AppData(),
),
// Data related to the FoodCategoriesData objects: (sqlite)
ChangeNotifierProvider<FoodCategoriesData>(
create: (context) => FoodCategoriesData(),
),
// Data related to the FoodCategoriesFoodRecipesData objects: (sqlite)
ChangeNotifierProvider<FoodCategoriesFoodRecipesData>(
create: (context) => FoodCategoriesFoodRecipesData(),
),
// Data related to the FoodRecipesData objects: (sqlite)
ChangeNotifierProvider<FoodRecipesData>(
create: (context) => FoodRecipesData(),
),
// Data related to the FoodIngredientsData objects: (sqlite)
ChangeNotifierProvider<FoodIngredientsData>(
create: (context) => FoodIngredientsData(),
),
// Data related to the RecipeStepsData objects: (sqlite)
ChangeNotifierProvider<RecipeStepsData>(
create: (context) => RecipeStepsData(),
),
],
// child: MyApp(),
child: InitialSplashScreen(),
));
}
- On each screen that includes your Extra Navigation (that shows the BottomNavigationBar) you will add with RouteAware and RouteObserverMixin to your State.
Example:
.
.
.
class _FoodCategoryIndexScreenState extends State<FoodCategoryIndexScreen> with RouteAware, RouteObserverMixin {
.
.
.
On each of those stateful screens (except on FavoritesScreen) we will add this property to the local state:
int _activeTab = 0;
Note: On the FavoritesScreen this will be 1 by default , instead of 0, of course, because Favorites is the 2nd tab (index = 1). So in FavoritesScreen is like this:
int _activeTab = 1;
- Then you will override the RouteAware methods (we only need didPopNext and didPush) on each of those stateful screens:
/// Called when the top route has been popped off, and the current route
/// shows up.
@override
void didPopNext() {
print('didPopNext => Emerges: $_screenId');
setState(() {
_activeTab = 0;
});
}
/// Called when the current route has been pushed.
@override
void didPush() {
print('didPush => Arriving to: $_screenId');
setState(() {
_activeTab = 0;
});
}
/// Called when the current route has been popped off.
@override
void didPop() {
print('didPop => Popping of: $_screenId');
}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
@override
void didPushNext() {
print('didPushNext => Covering: $_screenId');
}
Of course, in FavoritesScreen is like this:
/// Called when the top route has been popped off, and the current route
/// shows up.
@override
void didPopNext() {
print('didPopNext => Emerges: $_screenId');
setState(() {
_activeTab = 1;
});
}
/// Called when the current route has been pushed.
@override
void didPush() {
print('didPush => Arriving to: $_screenId');
setState(() {
_activeTab = 1;
});
}
/// Called when the current route has been popped off.
@override
void didPop() {
print('didPop => Popping of: $_screenId');
}
/// Called when a new route has been pushed, and the current route is no
/// longer visible.
@override
void didPushNext() {
print('didPushNext => Covering: $_screenId');
}
The RouterObserver package will keep track of all the pushings and poppings and execute accordingly the proper method, based on each back swipe or back link clicked by the user, therefore updating accordingly the _activeTab property on each stateful screen.
- And then we will simply pass that int _activeTab property, as a named parameter, to each FeeddyScaffold inside each stateful screen. Like this:
lib/screens/food_category_index_screen.dart
.
.
.
return FeeddyScaffold(
activeIndex: _activeTab,
appTitle: widget.appTitle,
innerWidgets: [
// Food Categories Grid:
Expanded(
flex: 5,
child: FoodCategoriesGrid(),
),
],
objectsLength: amountTotalFoodCategories,
objectName: 'category',
onPressedAdd: () => _showModalNewFoodCategory(context),
);
}
.
.
.
Each updating of the _activeTab property based on a setState execution, will always provoque a re-rendering of the UI, which will allow to always show accordingly the proper tab index on the BottomNavigationBar, based on when we are, matching always the current route with the active tab. In this case we want to show always active the first tab, except when are viewing the FavoritesScreen.
So, it would look like this always, in a very consistent way, no matter if we are either pushing or popping the routes:
For more details, you can clone the source code of my app at GitHub from here.
THE END.

- 1,002
- 1
- 11
- 19
For who is looking for a short solution here is mine, maybe it will be useful for someone:
class App extends StatefulWidget {
@override
State<StatefulWidget> createState() => AppState();
}
class AppState extends State<App> {
int currentTab = 0;
void _selectTab(int index) {
debugPrint (" index = $index ");
setState(() {
currentTab = index;
});
switch (currentTab) {
case 0:
debugPrint (" my index 0 ");
break;
case 1:
debugPrint (" my index 1 ");
break;
case 2:
debugPrint (" my index 2 ");
break;
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: _buildBody(),
bottomNavigationBar: BottomNavigationBar(
currentIndex: currentTab,
onTap: _selectTab,
items: [
BottomNavigationBarItem(
icon: Icon(Icons.home), title: Text("Home"),
),
BottomNavigationBarItem(
icon: Icon(Icons.message), title: Text("Message"),
),
BottomNavigationBarItem(
icon: Icon(Icons.settings), title: Text("Settings"),
),
],
),
);
}
Widget _buildBody() {
// return a widget representing a page
}
}
And we dont forget the main, should be like this:
import 'package:flutter/material.dart';
import 'App.dart';
void main() {
runApp( MaterialApp(
home: App(),
)
);
}

- 5
- 4