69

The issue:

I have 2 tabs using Default Tabs Controller, like so:

Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        drawer: Menu(),
        appBar: AppBar(
          title: Container(
            child: Text('Dashboard'),
          ),
          bottom: TabBar(
            tabs: <Widget>[
              Container(
                padding: EdgeInsets.all(8.0),
                child: Text('Deals'),
              ),
              Container(
                padding: EdgeInsets.all(8.0),
                child: Text('Viewer'),
              ),
            ],
          ),
        ),
        body: TabBarView(
          children: <Widget>[
            DealList(),
            ViewersPage(),
          ],
        ),
      ),
    );
  }
}

The DealList() is a StatefulWidget which is built like this:

Widget build(BuildContext context) {
    return FutureBuilder(
      future: this.loadDeals(),
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        print('Has error: ${snapshot.hasError}');
        print('Has data: ${snapshot.hasData}');
        print('Snapshot data: ${snapshot.data}');
        return snapshot.connectionState == ConnectionState.done
            ? RefreshIndicator(
                onRefresh: showSomething,
                child: ListView.builder(
                  physics: const AlwaysScrollableScrollPhysics(),
                  itemCount: snapshot.data['deals'].length,
                  itemBuilder: (context, index) {
                    final Map deal = snapshot.data['deals'][index];
                    print('A Deal: ${deal}');
                    return _getDealItem(deal, context);
                  },
                ),
              )
            : Center(
                child: CircularProgressIndicator(),
              );
      },
    );
  }
}

With the above, here's what happens whenever I switch back to the DealList() tab: It reloads.

Flutter

Is there a way to prevent re-run of the FutureBuilder when done once? (the plan is for user to use the RefreshIndicator to reload. So changing tabs should not trigger anything, unless explicitly done so by user.)

KhoPhi
  • 9,660
  • 17
  • 77
  • 128

1 Answers1

144

There are two issues here, the first:

When the TabController switches tabs, it unloads the old widget tree to save memory. If you want to change this behavior, you need to mixin AutomaticKeepAliveClientMixin to your tab widget's state.

class _DealListState extends State<DealList> with AutomaticKeepAliveClientMixin<DealList> {
  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context); // need to call super method.
    return /* ... */
  }
}

The second issue is in your use of the FutureBuilder - If you provide a new Future to a FutureBuilder, it can't tell that the results would be the same as the last time, so it has to rebuild. (Remember that Flutter may call your build method up to once a frame).

return FutureBuilder(
  future: this.loadDeals(), // Creates a new future on every build invocation.
  /* ... */
);

Instead, you want to assign the future to a member on your State class in initState, and then pass this value to the FutureBuilder. The ensures that the future is the same on subsequent rebuilds. If you want to force the State to reload the deals, you can always create a method which reassigns the _loadingDeals member and calls setState.

Future<...> _loadingDeals;

@override
void initState() {
  _loadingDeals = loadDeals(); // only create the future once.
  super.initState();
}

@override
Widget build(BuildContext context) {
  super.build(context); // because we use the keep alive mixin.
  return new FutureBuilder(future: _loadingDeals, /* ... */);
}
Shirish Kadam
  • 689
  • 1
  • 9
  • 21
Jonah Williams
  • 20,499
  • 6
  • 65
  • 53
  • 2
    In fact you killed two of my problems with one stone. Thanks so much. Really on point and the explanation makes sense, just like the solution does. Working now. – KhoPhi Jul 08 '18 at 17:48
  • 5
    is there any solution without initState? Because I'm using streamBuilder with bloc pattern. all data fetch logic is inside my bloc class. – Yeahia2508 Dec 08 '18 at 12:49
  • check this for avoid initState, use `AsyncMemoizer::runOnce() ` https://medium.com/saugo360/flutter-my-futurebuilder-keeps-firing-6e774830bc2 – mixer Feb 07 '19 at 07:12
  • 11
    I tried to implement this in my app, having `bottomNavigationBar`, but whenever I change page, it still fires the API. – Suroor Ahmmad Aug 02 '19 at 14:59
  • Is it possible to get this working for max 3 tabs? The previous and the next one? – Robin Dijkhof Sep 12 '19 at 21:19
  • 4
    they could have at least provided a way to really prevent the tab from getting rebuilt. this solution still rebuilds the widget, but only runs the future once. – chitgoks Jun 12 '20 at 08:33
  • @Yeahia2508 did you find a solution for streamBluider ? – Fatima ayaa Mar 31 '21 at 16:42
  • save my day, thank you also to @Fatimaayaa for streamBuilder is just same as i use streamBuilder – EM Farih Jun 27 '21 at 11:43
  • @SuroorAhmmad use IndexedStack when switching tab in bottomNavigatorBar. It does not rebuilt your widget. Like this .. body: IndexedStack( index: currentIndex, children: [ home(), ], ), – Sachin Aug 15 '21 at 10:59