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!
Imagine the FutureBuilder’s parent is a ListView. This is what happens:
build fires many times per second to update the screencallAsyncFetch() gets invoked once per build returning new Futures every timedidUpdateWidget in the FutureBuilder compares old and new Futures; if different it calls the builder againbuilder 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)
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.
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:
StatelessWidget containing a FutureBuilder. It’s fed a new Future that resolves to the current date in secondsStatefulWidget 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 FutureBuilderHit 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.
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.
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...');
}
);
}
}
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!