Back to Blog
Aerial view of a construction worker in a hard hat holding a laptop, titled "Flutter Future Builder"
Engineering
Jun 29, 2020
4 Min Read

How to use Future Builders Effectively

You don’t usually use Futures directly in a Flutter view, but if you use them incorrectly, your app’s performance can suffer. This article explains the best way to handle Future values inside a Flutter widget.

What are Futures and FutureBuilders?

In simple terms, a Future is data that hasn’t arrived yet, and a FutureBuilder is a Widget that displays that data once it does. If you’d like a more detailed look at how FutureBuilder works, the article below covers it well.

How to get data from async function to show on a widget: a detailed look at different ways to use FutureBuilders in your app.

App Structure

For this example, we’ll use Firebase. The view will take a DocumentReference as a parameter, fetch the related data from Firestore, and show it inside a Scaffold. Because of this, the view should be a StatefulWidget. We’ll call this widget SecondPage.

class SecondPage extends StatefulWidget {
  final DocumentReference ref;

  const SecondPage({
    Key key,
    @required this.ref,
  }) : super(key: key);

  @override
  _SecondPageState createState() => _SecondPageState();
}

App State

That part was simple. Managing the state for this widget is a bit more challenging, especially if you haven’t handled state that depends on values passed into the widget before.

class _SecondPageState extends State<SecondPage> {

  // This variable will be used to store
  // Future data for this State
  Future<Map<String, dynamic>> data;

  @override
  Widget build(BuildContext context) {
     ...
  }
}

Next, we need to take the DocumentReference passed into the widget and assign it to the data variable. Here’s how you can do that:

@override
void initState() {
  super.initState();
  data = mapData();
}

Future<Map<String, dynamic>> mapData() async {
  return (await widget.ref.get()).data;
}

What if the page rebuilds?

This code fetches the data from the DocumentReference and stores it in the data variable as a Future. Because it is inside initState, it only runs once, the first time SecondPage is built in the widget tree.

But what if SecondPage is rebuilt with a different DocumentReference? Since initState only runs once, data does not get updated. You might pass the correct value into the widget, but the UI will not show it. If you are not aware of this, it can be a confusing bug to find. Here is how to fix it:

@override
void didUpdateWidget(SecondPage oldWidget) {
  super.didUpdateWidget(oldWidget);
  if (oldWidget.ref.path != widget.ref.path) {
    data = mapData();
  }
}

didUpdateWidget runs every time the Widget connected to this State, in this case SecondPage, rebuilds. It checks if the DocumentReference has changed and updates data if needed. You do not need to call setState after updating data here, because a rebuild will happen automatically. didUpdateWidget always runs as part of that rebuild process.

Showing the data

Now, the last step is to use a FutureBuilder to show the data when it arrives.

@override
Widget build(BuildContext context) {
  return Scaffold(
    body: FutureBuilder<Map<String, dynamic>>(
      future: data,
      builder: (context, snapshot) {
        if (snapshot.hasData) {
          final name = snapshot.data['name'];
          final email = snapshot.data['email'];
          return Column(
            children: <Widget>[
              Text("Name: $name"),
              Text("Email: $email"),
            ],
          );
        }
        return CircularProgressIndicator();
      }
    ),
  );
}

This code displays a CircularProgressIndicator while the data is loading, and then shows the data inside the Scaffold once it is available.

Why do we need the data variable?

At this point, you might wonder why you need the data variable at all. Could you remove it, make this a StatelessWidget, and call mapData() directly inside the FutureBuilder? Would it still fetch the data and work the same way?

It would fetch and show the data, but there is a catch. mapData() would run on every rebuild, calling Firestore and creating a new Future each time. This means every rebuild would show a CircularProgressIndicator again before the data appears, even if nothing changed. You only need to fetch new data when the DocumentReference changes, which is why you should avoid calling a Future-returning method directly inside build.

Here’s the complete widget, put together:

class SecondPage extends StatefulWidget {
  final DocumentReference ref;

  const SecondPage({
    Key key,
    @required this.ref,
  }) : super(key: key);

  @override
  _SecondPageState createState() => _SecondPageState();
}

class _SecondPageState extends State<SecondPage> {
  Future<Map<String, dynamic>> data;

  @override
  void initState() {
    super.initState();
    data = mapData();
  }

  @override
  void didUpdateWidget(SecondPage oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.ref.path != widget.ref.path) {
      data = mapData();
    }
  }

  Future<Map<String, dynamic>> mapData() async {
    return (await widget.ref.get()).data;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: FutureBuilder<Map<String, dynamic>>(
        future: data,
        builder: (context, snapshot) {
          if (snapshot.hasData) {
            final name = snapshot.data['name'];
            final email = snapshot.data['email'];
            return Column(
              children: <Widget>[
                Text("Name: $name"),
                Text("Email: $email"),
              ],
            );
          }
          return CircularProgressIndicator();
        }
      ),
    );
  }
}

This is the complete pattern: fetch data once in initState, refresh only when the DocumentReference changes in didUpdateWidget, and let FutureBuilder handle showing the result.

Join the Conversation

This dispatch is part of an ongoing series on the future of intelligence. Share your perspective or subscribe for more.

Weekly dispatches. No spam. Ever.