I. Overview of the problem

A problem I found with FutureBuilder was that the future function in FutrueBuilder would be executed every time the interface was refreshed. Although this didn’t cause any serious problems (or even required to do so in some cases), in some cases, This problem must be solved (explained later).

There is a related issue on Github: FutureBuilder Fire.

I came across a related article on Medium that explains this, and I share it with you.

Second, the recurrence of problems

The problem is simple to reproduce, as long as there is a FutureBuilder component in the page, then each call to setState causes FutureBuilder to refresh once and the future function to execute once.

Suppose our code looks like this:

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:
                    returnCenter( child: Text(snapshot.data) ); }}),],),); } _fetchData() async { await Future.delayed(Duration(seconds: 2));return 'REMOTE DATA'; }}Copy the code
  • SetState () is triggered by the Switch component state.
  • FutrueBuilder uses _fetchData() to simulate getting data from the server

Run the code above, and the following happens.

As you can see, every switch state change causes FutrueBuilder to refresh, but in the above code, there is no connection between Switch and FutureBuilder. Since any setState operation causes FutureBuilder to refresh once, the following problems may result:

  • Code executing when the interface is no longer visible (costing performance, traffic, etc.)
  • Thermal overloading is not correct
  • Updating values in the Inherited component will cause the Navigator state to be lost
  • etc…

3. Cause analysis

FutureBuilder is actually a StatefulWidget type component.

class FutureBuilder<T> extends StatefulWidget { /// Creates a widget that builds itself based on the latest snapshot of /// interaction with a [Future]. /// /// The [builder] must not be null. const FutureBuilder({ Key key, this.future, this.initialData, @required this.builder, }) : assert(builder ! = null), super(key: key); . }Copy the code

The StatefulWidgets component will hold a State object with a long lifetime that has a number of methods associated with the lifetime.

  • InitState: called once after the object is created.
  • Build This method is called every time we refresh the presentation component.
  • DidUpdateWidget Is called back when an old component is recycled and a new component is rebuilt, and the new Widget is bound to the current State object. To put it simply, the didUpdateWidget executes when the State object associated with a component changes, allowing you to perform some of the operations required before rebuilding.

In FutureBuilder, the didUpdateWidget method is rewritten as follows:

@override
void didUpdateWidget(FutureBuilder<T> oldWidget) {
  super.didUpdateWidget(oldWidget);
  if(oldWidget.future ! = widget.future) {if (_activeCallbackIdentity != null) {
      _unsubscribe();
      _snapshot = _snapshot.inState(ConnectionState.none);
    }
    _subscribe();
  }
}
Copy the code

If the future of the new widget is not the same object instance as the future of the old widget, this is repeated: _unsubscribe() and _subscribe(), which is the FutureBuilder’s life cycle all over again.

So the problem is that every time the interface refreshes, the future instance of the old state component of FutureBuilder is not the same as the FuTrue instance of the new state component, so what we need to do is, Let the old and new Components of FutureBuilder get the same Future instance.

Iv. Solutions

In some functional languages, when a function is specified, as long as the input is the same, the function must output the same result, so we can store the result, and when the function is called again, we simply return the result of the last run.

In our case, we need to store the fuTrue object instance and get the same instance every time to solve the above problem.

Dart provides a class called AsyncMemoizer that meets our needs.

To summarize, this class ensures that the function is executed only once, stores the result, and returns the result when the function is called multiple times.

To use this class, it adds the async library dependency (note that the dependency versions do not conflict):

Async: ^ 2.3.0Copy the code

We initialize an instance of the AsnycMemorizer object in our code:

final AsyncMemoizer _memoizer = AsyncMemoizer();
Copy the code

Finally, use the previously defined future functions in conjunction with memorizer:

_fetchData() {
  return this._memoizer.runOnce(() async {
    await Future.delayed(Duration(seconds: 2));
    return 'REMOTE DATA';
  });
Copy the code

The final effect is as follows:

Some of you might be wondering, if I want to control whether or not I want to refresh FutureBuilder every time I refresh, what should I do? I think we could add a variable that determines whether to return the same instance, and thus whether to refresh.

  _fetchData(bool flag) async{
    if(flag){
      return this._memoizer.runOnce(() async {
        await Future.delayed(Duration(seconds: 2));
        return 'REMOTE DATA';
      });
    }else{
      await Future.delayed(Duration(seconds: 5));
      return 'REMOTE DATA'; }}Copy the code


github

The last

Welcome to follow the wechat public account “Flutter Programming and Development”.