Why Is My Future/Async Called Multiple Times?

Last reviewed in December 2019 by Frank Treacy

Why is FutureBuilder firing multiple times? My future should be called just once!

It appears that this build method is rebuilding unnecessarily:

@override
Widget build(context) {
  return FutureBuilder<String>(
    future: callAsyncFetch(), // called all the time!!! ๐Ÿ˜ก
    builder: (context, snapshot) {
      // rebuilding all the time!!! ๐Ÿ˜ก
    }
  );
}

This causes unintentional network refetches, recomputes and rebuilds โ€“ which can also be an expensive problem if using Firebase, for example.

Well, let me tell you something…

This is not a bug ๐Ÿž, it's a feature โœ…!

Let's quickly see why… and how to fix it!

Understanding the problem

Imagine the FutureBuilder's parent is a ListView. This is what happens:

  • ๐Ÿงป User scrolls list
  • ๐Ÿ”ฅ build fires many times per second to update the screen
  • โ‘ callAsyncFetch() gets invoked once per build returning new Futures every time
  • = didUpdateWidget in the FutureBuilder compares old and new Futures; if different it calls the builder again
  • ๐Ÿ˜ฉ Since instances are always new (always different to the old one) the builder refires once for every call to the parent's build… that is, A LOT

(Remember: Flutter is a declarative framework. This means it will paint the screen as many times as needed to reflect the UI you declared, based on the latest state)

A quick fix ๐Ÿ”ง

We clearly must take the Future out of this build method!

A simple approach is by introducing a StatefulWidget where we stash our Future in a variable. Now every rebuild will make reference to the same Future instance:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  Future<String> _future;

  @override
  void initState() {
    _future = callAsyncFetch();
    super.initState();
  }

  @override
  Widget build(context) {
    return FutureBuilder<String>(
      future: _future,
      builder: (context, snapshot) {
        // ...
      }
    );
  }
}

We're caching a value (in other words, memoizing) such that the build method can now call our code a million times without problems.

Live example

Here we have a sample parent widget that rebuilds every 3 seconds. It's meant to represent any widget that triggers rebuilds like, for example, a user scrolling a ListView.

The screen is split in two:

  • Top: a StatelessWidget containing a FutureBuilder. It's fed a new Future that resolves to the current date in seconds
  • Bottom: a StatefulWidget containing a FutureBuilder. A new Future (that also resolves to the current date in seconds) is cached in the State object. This cached Future is passed into the FutureBuilder

Hit Run and see the difference (wait at least 3 seconds). Rebuilds are also logged to the console.



The top future (stateless) gets called and triggered all the time (every 3 seconds in this example).

The bottom (stateful) can be called any amount of times without changing.

Cleaner ways ๐Ÿ›€

Are you using Provider by any chance? You can simply use a FutureProvider instead of the StatefulWidget above:

class MyWidget extends StatelessWidget {
  // Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
  @override
  Widget build(BuildContext context) {
    // print('building widget');
    return FutureProvider<String>(
      create: (_) {
        // print('calling future');
        return callAsyncFetch();
      },
      child: Consumer<String>(
        builder: (_, value, __) => Text(value ?? 'Loading...'),
      ),
    );
  }
}

Much nicer, if you ask me.

Tip! It's a fully functional example. Comment out those lines and try it out in your own editor!

Another option is using the fantastic Flutter Hooks library with the useMemoized hook for the memoization (caching):

class MyWidget extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final future = useMemoized(() {
      // Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
      callAsyncFetch(); // or your own async function
    });
    return FutureBuilder<String>(
      future: future,
      builder: (context, snapshot) {
        return Text(snapshot.hasData ? snapshot.data : 'Loading...');
      }
    );
  }
}

Takeaway

Your build methods should always be pure, that is, never have side-effects (like updating state, calling async functions).

Remember that builders are ultimately called by build!

The best from the Flutter-verse in 3 minutes or less? Join Snacks!

Delivered twice monthly. No link walls. No spam. EVER.