How to Build Widgets with an Async Method Call

Last reviewed in December 2019 by Frank Treacy

You want to return a widget in a build method…

But your data comes from an async function!

class MyWidget extends StatelessWidget {
  @override
  Widget build(context) {
    callAsyncFetch().then((data) {
      return Text(data);  // doesn't work
    });
  }
}

The callAsyncFetch function could be an HTTP call, a Firebase call, or a call to SharedPreferences or SQLite, etc. Anything that returns a Future 🔮.

So, can we make the build method async? 🤔

class MyWidget extends StatelessWidget {
  @override
  Future<Widget> build(context) async {
    var data = await callAsyncFetch();
    return Text(data);  // doesn't work either
  }
}

Not possible! A widget's build “sync” method will NOT wait for you while you fetch data 🙁

(You might even get a type 'Future' is not a subtype of type kind of error.)

🛠 How do we fix this with Flutter best practices?

Meet FutureBuilder:

class MyWidget extends StatelessWidget {
  @override
  Widget build(context) {
    return FutureBuilder<String>(
      future: callAsyncFetch(),
      builder: (context, AsyncSnapshot<String> snapshot) {
        if (snapshot.hasData) {
          return Text(snapshot.data);
        } else {
          return CircularProgressIndicator();
        }
      }
    );
  }
}

It takes our Future as argument, as well as a builder (it's basically a delegate called by the widget's build method). The builder will be called immediately, and again when our future resolves with either data or an error.

An AsyncSnapshot<T> is simply a representation of that data/error state. This is actually a useful API!

If we get a new snapshot with:

  • 📭 no data… we show a progress indicator
  • data from our future… we use it to feed any widgets for display!
  • error from our future… we show an appropriate message
Do you think the answer to this problem is a StatefulWidget? Yes, it's a possible solution but not an ideal one. Keep on reading and we'll see why.

Click Run and see it for yourself!



It will show a circular progress indicator while the future resolves (about 2 seconds) and then display data. Problem solved!

🎩 Under the hood: FutureBuilder

FutureBuilder itself is built on top of StatefulWidget! Attempting to solve this problem with a StatefulWidget is not wrong but simply lower-level and more tedious.

Check out the simplified and commented-by-me source code:

(I removed bits and pieces for illustration purposes)

// FutureBuilder *is* a stateful widget
class FutureBuilder<T> extends StatefulWidget {
  
  // it takes in a `future` and a `builder`
  const FutureBuilder({
    this.future,
    this.builder
  });

  final Future<T> future;

  // the AsyncWidgetBuilder<T> type is a function(BuildContext, AsyncSnapshot<T>) which returns Widget
  final AsyncWidgetBuilder<T> builder;

  @override
  State<FutureBuilder<T>> createState() => _FutureBuilderState<T>();
}

class _FutureBuilderState<T> extends State<FutureBuilder<T>> {
  // keeps state in a local variable (so far there's no data)
  AsyncSnapshot<T> _snapshot = null;

  @override
  void initState() {
    super.initState();
    
    // wait for the future to resolve:
    //  - if it succeeds, create a new snapshot with the data
    //  - if it fails, create a new snapshot with the error
    // in both cases `setState` will trigger a new build!
    widget.future.then<void>((T data) {
      setState(() { _snapshot = AsyncSnapshot<T>(data); });
    }, onError: (Object error) {
      setState(() { _snapshot = AsyncSnapshot<T>(error); });
    });
  }

  // builder is called with every `setState` (so it reacts to any event from the `future`)
  @override
  Widget build(BuildContext context) => widget.builder(context, _snapshot);

  @override
  void didUpdateWidget(FutureBuilder<T> oldWidget) {
    // compares old and new futures!
  }

  @override
  void dispose() {
    // ...
    super.dispose();
  }
}

Very simple, right? This is likely similar to what you tried when using a StatefulWidget. Of course, for the real, battle-tested source code see FutureBuilder.

Before wrapping up… 🎁

From the docs

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.

Widget build(context) {
  return FutureBuilder<String>(
    future: callAsyncFetch(),

Does this mean callAsyncFetch() will be called many times?

In this small example, there is no reason for the parent to rebuild (nothing changes) but in general you should assume it does. See Why is my Future/Async Called Multiple Times?.

Did this help you understand how to work with Futures in Flutter? Let us know 🙌

The best from the Flutter-verse in 3 minutes or less? Join Snacks!

Delivered twice monthly. No link walls. No spam. EVER.