0

I have implemented the multiple Offstage Navigators for the bottomNavigationBar, further details can be seen here. The problem is that when using this method, each time we select a bottom navigation item, the FutureBuilder runs the future method and rebuilds the entire widget, each Offstage widget and all their children are also rebuilt.

For each Offstage widget, I'm loading data via html request and that means each time I switch a tab, 5 requests will be made.

This is my main Scaffold which holds the bottomNavigationBar.

@override
  Widget build(BuildContext context) {
    TabProvider tabProvider = Provider.of<TabProvider>(context);
    return FutureBuilder(
      future: initProvider(),
      builder: (context, snapshot) {
        if (snapshot.connectionState == ConnectionState.done) {
          return WillPopScope(
            onWillPop: _onWillPop,
            child: Scaffold(
              appBar: AppBar(
                title: Text(tabName[tabProvider.currentTab],
              ),
              body: Stack(children: <Widget>[
                _buildOffstageNavigator(TabItem.feed, tabProvider.currentTab),
                _buildOffstageNavigator(TabItem.explore, tabProvider.currentTab),
                _buildOffstageNavigator(TabItem.guide, tabProvider.currentTab),
                _buildOffstageNavigator(TabItem.map, tabProvider.currentTab),
                _buildOffstageNavigator(TabItem.profile, tabProvider.currentTab),
              ]),
              bottomNavigationBar: BottomNavigation(
                currentTab: tabProvider.currentTab,
                onSelectTab: tabProvider.selectTab,
              ),
            ),
          );
        } else {
          return Text('Loading');
        }
      },
    );
  }

The FutureBuilder will initialize the values in my provider so each tab can access the cached data.

The _buildOffstageNavigator will return the below

return Offstage(
      offstage: currentTab != tabItem,
      child: TabNavigator(
        navigatorKey: navigatorKeys[tabItem],
        tabItem: tabItem,
      ),
    );

Below is the Widget which is built inside the Scaffold body and hence inside the Offstage Navigator from above.

@override   
  Widget build(BuildContext context) {
    TabProvider tabProvider = Provider.of<TabProvider>(context);
    States stateData = tabProvider.exploreStateCache;
    return Container(
      child: ListView(
        children: <Widget>[
          Text(stateData.stateName),
          Text(stateData.stateDescription),
        ],
      ),
    );
  }
        

I have followed this articles advice for using futures with the provider but something else is missing

Levy77
  • 219
  • 1
  • 9

1 Answers1

1

Instead of creating futures in a build method, which as you have noticed, may be called several times, create them in a place that is invoked only once. For example, a StatefulWidget's initState:

class Foo extends StatefulWidget {
  @override
  _FooState createState() => _FooState();
}

class _FooState extends State<Foo> {
  Future<MyData> _dataFuture;

  @override
  void initState() {
    super.initState();
    _dataFuture = getData();
  }

  @override
  Widget build(BuildContext context) => FutureBuilder<MyData>(
        future: _dataFuture,
        builder: (context, snapshot) => ...,
      );
}

A second thing you can improve is reduce the scope of what gets rebuild when the provider provides a new value for TabProvider. The context that you call Provider.of<Data>(context) gets rebuild when there's a new value for Data. That is done most conveniently with the various other widgets offered by the provider package, like Consumer and Selector.

So remove the Provider<TabProvder>.of(context) calls and use Consumers and Selectors. For example, to only rebuild the title when a tab is switched:

AppBar(
  title: Selector<TabProvider, String>(
    selector: (context, tabProvider) => tabName[tabProvider.currentTab],
    builder: (context, title) => Text(title),
  ),
)

Selector only rebuilds the Text(title) widget, when the result of its selector callback is different from the previous value. Similarly for _buildOffstageNavigator:

Widget _buildOffstageNavigator(BuildContext context, TabItem tabItem) {
  return Selector<TabProvider, bool>(
    selector: (context, tabProvider) => tabProvider.currentTab != tabItem,
    builder: (context, isCurrent) => Offstage(
      offstage: isCurrent,
      child: Selector<TabProvider, Key>(
        selector: (context, tabProvider) => tabProvider.navigatorKeys[tabItem],
        builder: (context, tabKey) => TabNavigator(
          navigatorKey: tabKey,
          tabItem: tabItem,
        ),
      ),
    );
}

(Beware: All code is untested and contains typos)

spkersten
  • 2,849
  • 21
  • 20
  • As the `_dataFuture = getData();` will initialize the variables used by the provider, the init state function cannot use the Provider.of(context). Is there another way around this? – Levy77 Jan 10 '21 at 16:24
  • If you need to use context in initState, you can move the code to didChangeDependencies and make it lazily initialize the variable. For example: `variableInState ??= Provider.of(context, listen: false);` (only once) or `variableInState = Provider.of(context);` (widget gets rebuild when Data changes). I’m not sure this helps, as I don’t understand what you mean exactly. – spkersten Jan 10 '21 at 16:52