When to use FutureBuilder in Flutter

Viewed 3624

I would like to know when to use a FutureBuilder and how is it usefull, given the fact that a widget can be built multiple times during its life time and not only when we setState or update it, so instead of using below code:

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool done = false;

  @override
  void initState() {
    wait();
    super.initState();
  }

  Future<void> wait() async {
    await Future.delayed(Duration(seconds: 2));
    setState(() {
      done = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('is built');
    return done ? Scaffold(
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '',
              style: Theme.of(context).textTheme.headline4,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {},
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    ) : Scaffold(body: CircularProgressIndicator(),);
  }
}

In which cases would a FutureBuilder work for me instead of the above set up, given the fact that I would also want to optimize my app for less backend reads (in FutureBuilder I would read more than once). I am looking for a case where FutureBuilder would be more usefull and correct than the above setup.

6 Answers

FutureBuilder is used to handle "asynchronous" calls that return the data you want to display.

For example, let's say we have to get a List<String> from the server.

The API call :

  Future<List<String>> getStringList() async {
    try {
      return Future.delayed(Duration(seconds: 1)).then(
        (value) => ['data1', 'data2', 'data3', 'data4'],
      );
    } catch (e) {
      throw Exception(e);
    }
  }

How can we handle the above API call status (load, data, error...etc) using FutureBuilder:

FutureBuilder<List<String>?>(
        future: getStringList(),
        builder: (context, snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.waiting:
              return Center(
                child: CircularProgressIndicator(),
              );
            case ConnectionState.done:
              if (snapshot.hasError)
                return Text(snapshot.error.toString());
              else
                return ListView(
                  children: snapshot.data!.map((e) => Text(e)).toList(),
                );

            default:
              return Text('Unhandle State');
          }
        },
      ),

As we can see, we don't have to create a state class with variable isLoading bool and String for error...etc. FutureBuilder saves us time.

BUT

Since FutureBuilder is a widget and lives in the widget tree, rebuilding can make FutureBuilder run again.

So I would say you can use FutureBuilder when you know there won't be a rebuild, however there are many ways (in the internet) to prevent FutureBuilder from being called again when the rebuild happens but it didn't work for me and leads to unexpected behavior.

Honestly I prefer handling the state in a different class with any state management solution than using FutureBuilder because it would be safer (rebuild wont effect it), more usable and easier to read (spreating business logic from UI).

FutureBuilder

Widget that builds itself based on the latest snapshot of interaction with a Future.

The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies. It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.

If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.A general guideline is to assume that every build method could get called every frame, and to treat omitted calls as an optimization.

Documentation is very great way to get started and understand what widget does what in what condition...

https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html

Actually, you will never need to use FutureBuilder Widget if you don't want to. Your logic in your code do exactly what FutureBuilder Widget does if you optimise FutureBuilder Widget correctly.

This code is exactly same with yours:

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  bool done = false;
  late Future myFuture;

  @override
  void initState() {
    myFuture = wait();
    super.initState();
  }

  Future<bool> wait() async {
    await Future.delayed(const Duration(seconds: 2));
    return true;
  }

  @override
  Widget build(BuildContext context) {
    print('is built');
    return FutureBuilder(
        future: myFuture,
        builder: (BuildContext context, snapshot) {
           if(snapshot.connectionState == ConnectionState.waiting) {
              return const Scaffold(body: CircularProgressIndicator(),);
           } else {
              return Scaffold(
                 body: Center(
                    child: Column(
                       mainAxisAlignment: MainAxisAlignment.center,
                       children: <Widget>[
                          const Text(
                             'You have pushed the button this many times:',
                          ),
                          Text(
                              '',
                              style: Theme.of(context).textTheme.headline4,
                          ),
                       ],
                     ),
                   ),
                floatingActionButton: FloatingActionButton(
                   onPressed: () {},
                   tooltip: 'Increment',
                   child: const Icon(Icons.add),
                ),
             );
           }
        }
     );
  }
}

FutureBuilder is just a StatefulWidget whose state variable is _snapshot

Initial state is _snapshot = AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData);

FutureBuilder is generally used to remove boilerplate code.

consider you want to fetch some data from the backend on page launch and show a loader until data comes.

It is subscribing to future which we send via the constructor and update the state based on that.

Tasks for FutureBuilder:

  • Give the async task in future of Future Builder
  • Based on connectionState, show message (loading, active(streams), done)
  • Based on data(snapshot.hasError), show view

Benefits of FutureBuilder

  • Does not use the two state variables and setState
  • Reactive programming (FutureBuilder will take care of updating the view on data arrival)

Example:

FutureBuilder<String>(
future: _fetchNetworkCall, // async work
builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
   switch (snapshot.connectionState) {
     case ConnectionState.waiting: return Text('Loading....');
     default:
       if (snapshot.hasError)
          return Text('Error: ${snapshot.error}');
       else
      return Text('Result: ${snapshot.data}');
    }
  },
)

hence to conclude Future builder is wrapper/boilerplate of what we do typically, thus there should not be any performance impact.

One of the use cases is FutureBuilder update on a particular section inside the widget tree, not the full widget tree like you implement in the sample.

  Future<void> wait() async {
    return Future.delayed(Duration(seconds: 2));

}

Here setState() => update widget tree. So same code in FutureBuilder you can change the UI value in a particular position in the widget tree.

Related