Flutter: Laggy performance with PageView when updating the currentIndex in a BottomNavigationBar (but no lag if I don't update the currentIndex)

2,852

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).

Share:
2,852
VMX
Author by

VMX

Updated on December 23, 2022

Comments

  • VMX
    VMX over 1 year

    I'm creating an app with a Scaffold that contains:

    • A FutureBuilder in the body that creates a PageView as its child when data is loaded.
    • A 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.