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 Future
s every timedidUpdateWidget
in the FutureBuilder
compares old and new Future
s; 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 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.
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 builder
s are ultimately called by build
!