I'm creating an app with a Scaffold that contains:
FutureBuilder
in the body that creates a PageView
as its child when data is loaded.BottomNavigationBar
that syncs with the PageView
for a more intuitive navigation.Functionality-wise, everything works fine. I can swipe left and right between pages and the currentIndex
gets updated correctly in the BottomNavigationBar
, and if I tap on the BottomNavigationBar
elements the PageView
will animate to the correct page as expected.
However... performance is really bad when switching between pages, even in Profile mode.
After a lot of investigation, I've confirmed that lag is only present if I update the currentIndex
of the BottomNavigationBar
.
If I don't update the BottomNavigationBar
, animations remain very smooth when switching between pages, both when swiping on the PageView
and when tapping on the BottomNavigationBar
elements themselves.
I can also confirm that this happens exactly the same when using setState
and when using Provider
. I was really hoping that it was just the setState
method being inefficient... but no luck :(
For the setState
impementation, this is what I'm doing:
On the PageView
:
onPageChanged: (page) {
setState(() {
_selectedIndex = page;
});
}
On the BottomBarNavigation:
onTap: _onTappedBar,
currentIndex: _selectedIndex
and below:
void _onTappedBar(int value) {
_pageController.animateToPage(value, duration: Duration(milliseconds: 500), curve: Curves.ease);
setState(() {
_selectedIndex = value;
});
}
If I comment out both setState
methods, the app becomes buttery smooth again and I can use the BottomNavigationBar
correctly as well - it just doesn't update the selected item.
Interestingly enough, if I ONLY comment out the line inside both setState
methods (_selectedIndex = page;
and _selectedIndex = value;
) but leave the methods there, the app still lags all the same even though the setState
methods are completely empty and aren't updating anything...??
And this is the Provider
version:
On the PageView
:
onPageChanged: (page) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = page;
}
On the BottomBarNavigation
:
onTap: _onTappedBar,
currentIndex: Provider.of<BottomNavigationBarProvider>(context).currentIndex,
and below:
void _onTappedBar(int value) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = value;
pageController.animateToPage(value, duration: Duration(milliseconds: 500), curve: Curves.ease);
}
As said, just as laggy as the setState
version :(
Any idea of what's causing this lag and how to fix this? I really don't know what else to try.
Ok, so I think I managed to solve it while also learning a valuable lesson about Flutter!
I was on the right track with the setState
/ Provider
dilemma - you do need to use Provider
(or another state management solution) if you want to avoid rebuilding the whole page.
However, that's not enough.
In order to leverage the modularity of that implementation, you ALSO need to extract the relevant widget (in this case, the whole BottomNavigationBar
) outside the main widget. If you don't, it seems everything on the main page will still get rebuilt, even if only a small widget is listening for Provider
notifications.
So this is the structure of my root_screen
's build
method now (simplified body contents for readaibility):
Widget build(BuildContext context) {
return Scaffold(
body: PageView(
controller: _pageController,
children: <Widget>[
HomeScreen(),
PerformanceScreen(),
SettingsScreen(),
],
onPageChanged: (page) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = page;
},
);
bottomNavigationBar: MyBottomNavigationBar(onTapped: _onTappedBar),
);
}
Notice how the bottomNavigationBar:
parameter is no longer defined in this root_screen
. Instead, I've created a new class (a StatelessWidget
) in a separate Dart file that takes in an onTapped
function as a parameter, and I'm instantiating it from here.
Said _onTappedBar
function is defined right here on the root_screen
, just below the build
method:
void _onTappedBar(int value) {
Provider.of<BottomNavigationBarProvider>(context, listen: false).currentIndex = value;
_pageController.animateToPage(value, duration: Duration(milliseconds: 500), curve: Curves.ease);
}
And this is the separate Dart file containing the new MyBottomNavigationBar
class:
class MyBottomNavigationBar extends StatelessWidget {
@override
const MyBottomNavigationBar({
Key key,
@required this.onTapped,
}) : super(key: key);
final Function onTapped;
Widget build(BuildContext context) {
return BottomNavigationBar(
items: [
BottomNavigationBarItem(icon: Icon(Icons.home), title: Text('Home')),
BottomNavigationBarItem(
icon: Icon(Icons.trending_up), title: Text('Performance')),
BottomNavigationBarItem(
icon: Icon(Icons.settings), title: Text('Settings')),
],
onTap: onTapped,
currentIndex:
Provider.of<BottomNavigationBarProvider>(context).currentIndex,
);
}
}
Also for completeness (and because I absolutely needed to know), I tried using the setState
approach again while keeping the BottomNavigationBar
in its new separate file. I wanted to understand if simply extracting the widgets was enough to do the trick, or if you still need to use a state management solution no matter what.
It turns out... it wasn't enough! Performance using setState was horrible again, even though the BottomNavigationBar widget was extracted in its own class file.
So bottom line, in order to keep your app efficient and animations smooth, remember to extract widgets and modularise your Flutter code as much as possible, as well as using a state management solution instead of setState
. That seems to be the only way to avoid unnecessary redraws (and your code will obviously be much cleaner and easier to debug).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With