Flutter: Laggy performance with PageView when updating the currentIndex in a BottomNavigationBar (but no lag if I don't update the currentIndex)
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).
VMX
Updated on December 23, 2022Comments
-
VMX over 1 year
I'm creating an app with a Scaffold that contains:
- A
FutureBuilder
in the body that creates aPageView
as its child when data is loaded. - A
BottomNavigationBar
that syncs with thePageView
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 theBottomNavigationBar
, and if I tap on theBottomNavigationBar
elements thePageView
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 theBottomNavigationBar
.If I don't update the
BottomNavigationBar
, animations remain very smooth when switching between pages, both when swiping on thePageView
and when tapping on theBottomNavigationBar
elements themselves.I can also confirm that this happens exactly the same when using
setState
and when usingProvider
. I was really hoping that it was just thesetState
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 theBottomNavigationBar
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 thesetState
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.
- A