If you’ve ever done any serious Flutter development, then you have definitely faced the problem in the title above. But don’t worry, You’re not alone!

What is the problem?

The FutureBuilder widget that Flutter provides us to create widgets based on the state of some future, keeps re-firing that future every time a rebuild happens! The problem is best manifested with this simple example:

void main() => runApp(MyApp());



class MyApp extends StatelessWidget {

@override

Widget build(BuildContext context) {

return MaterialApp(

home: HomeScreen()

);

}

}



class HomeScreen extends StatefulWidget {

@override

_HomeScreenState createState() => _HomeScreenState();

}



class _HomeScreenState extends State<HomeScreen> {



bool _switchValue;



@override

void initState() {

super.initState();

this._switchValue = false;

}



@override

Widget build(BuildContext context) {

return Scaffold(

body: Column(

mainAxisAlignment: MainAxisAlignment.center,

children: <Widget>[

Switch(

value: this._switchValue,

onChanged: (newValue) {

setState(() {

this._switchValue = newValue;

});

},

),

FutureBuilder(

future: this._fetchData(),

builder: (context, snapshot) {

switch (snapshot.connectionState) {

case ConnectionState.none:

case ConnectionState.waiting:

return Center(

child: CircularProgressIndicator()

);

default:

return Center(

child: Text(snapshot.data)

);

}

}

),

],

),

);

}



_fetchData() async {

await Future.delayed(Duration(seconds: 2));

return 'REMOTE DATA';

}

}

Our app displays a HomeScreen on which we have one Switch widget and one FutureBuilder that loads remote data and displays it on the screen.

on which we have one widget and one that loads remote data and displays it on the screen. The FutureBuilder subscribes to the future returned from the function _fetchData . This function simulates a server that returns its result after two seconds.

subscribes to the future returned from the function . This function simulates a server that returns its result after two seconds. When the user taps the switch, we update the member _switchValue , which is then fed into the switch itself, to change its actual value.

If you run this app, the following happens:

Flipping the switch affects the state of the FutureBuilder

Even though the switch and the FutureBuilder are not related in any form, every time we change the switch value (by calling setState ), the FutureBuilder goes through its whole life-cycle again! It re-fetches the future, causing unneeded traffic, and shows the loading again, causing bad user experience.

This problem manifests itself in a variety of ways. In some cases it’s not even as obvious as the example above. For example:

Network traffic being generated from pages that are not currently on screen

Hot reloading not working properly

Losing the Navigator state when updating values in some Inherited Widgets

etc…

But what is the cause of all of this? And how can we solve it?

The didUpdateWidget problem

Note: In this section I will take a closer look at how FutureBuilder works. If you’re not interested in this, you can skip to the solution.

If we take a closer look at the code of FutureBuilder , we find that it is a StatefulWidget . As we know, StatefulWidgets maintain a long-lived State object. This state has a few methods that manage its life-cycle, like initState , build , and didUpdateWidget . initState is called only once when the state object is first created, and build is called every time we need to build the widget to display, but what what about didUpdateWidget ? This method is called whenever the widget attached to this State object changes. When the widget is rebuilt with new inputs, the old widget is disposed, and a new widget is created and assigned to the State object, and didUpdateWidget is called to do anything we want to do before the rebuild. In case of FutureBuilder , this method looks like this:

@override

void didUpdateWidget(FutureBuilder<T> oldWidget) {

super.didUpdateWidget(oldWidget);

if (oldWidget.future != widget.future) {

if (_activeCallbackIdentity != null) {

_unsubscribe();

_snapshot = _snapshot.inState(ConnectionState.none);

}

_subscribe();

}

}

It’s basically saying: if, when rebuilt, the new widget has a different Future instance than the old one, then repeat everything: unsubscribe, and subscribe again.

Well, that’s the key! If we provide it with the same future, then it will not refire!

But aren’t we providing the same future? we’re calling the same function! well, it’s not the same future instance. Our function is doing exactly the same work, but then returning a new future, different from the old one.

So, what we want to do is to store or cache the output of the function the first time it is called, and then provide this same output whenever the function is called again. This process is known as memoization.

Solution: Memoize the future

Memoization is, in simple terms, caching the return value of a function, and reusing it when that function is called again. Memoization is mostly used in functional languages, where functions are deterministic (they always return the same output for the same inputs), but we can use simple memoization for our problem here, to make sure the FutureBuilder always receives the same future instance.

To do that, we will use Dart’s AsyncMemoizer . This memoizer does exactly what we want! It takes an asynchronous function, calls it the first time it is called, and caches its result. For all subsequent calls to the function, the memoizer returns the same previously calculated future.

Thus, to solve our problem, we start by creating an instance of AsyncMemoizer in our widget:

final AsyncMemoizer _memoizer = AsyncMemoizer();

Note: you shouldn’t instantiate the memoizer inside a StatelessWidget , because Flutter disposes of StatelessWidgets at every rebuild, which basically beats the purpose. You should instantiate it either in a StatefulWidget, or somewhere where it can persist.

Afterwards, we will modify our _fetchData function to use that memoizer:

_fetchData() {

return this._memoizer.runOnce(() async {

await Future.delayed(Duration(seconds: 2));

return 'REMOTE DATA';

});

}

We wrapped our function with AsyncMemoizer.runOnce which does exactly what it sounds like; it runs the function only once, and when called again, returns the cached future.