DEV Community

Cover image for Displaying async values in Flutter
Emir Cihangir
Emir Cihangir

Posted on • Originally published at emircihangir.com

Displaying async values in Flutter

The build method in Flutter widgets is synchronous. That means it doesn’t like to wait for anything. But sometimes, we need to wait for a value to arrive in order to display it.

Let’s think of a simple weather app that displays only the temperature of a city. The app needs to make a request to the backend, get the temperature value, and finally display it. It will have to wait for a response from the backend, but as we discussed, the build method does not like to wait for anything. So how do we solve this issue?

Enter: FutureBuilder. FutureBuilder takes a value of type Future and displays widgets until it is resolved. In fact, we can specify which widgets to display not only while loading but also when an error occurs. Let’s see how we can use FutureBuilder in a simple app.

First, create an app in a directory of your choice:

flutter create future_builder --platforms=macos
Enter fullscreen mode Exit fullscreen mode

You can choose whichever platform you want.

Open the project in your preferred IDE, and navigate to lib/main.dart. Replace the entire content of the file with the following:

import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const MyHomePage());
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  Future<int> _getTemperature() async {
    await Future.delayed(Duration(seconds: 3)); // Dummy delay of three seconds.
    return 25;
  }

  Future<int> _getTemperatureError() async {
    await Future.delayed(Duration(seconds: 3));
    throw Exception('An error occurred while retrieving the temperature value.');
  }

  Future<int?> _getTemperatureEmpty() async {
    await Future.delayed(Duration(seconds: 3));
    return null;
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: FutureBuilder(
          future: _getTemperature(),
          builder: (context, snapshot) {
            if (snapshot.connectionState == ConnectionState.waiting) {
              return Column(
                mainAxisAlignment: MainAxisAlignment.center,
                spacing: 16,
                children: [
                  const CircularProgressIndicator(),
                  const Text('Loading temperature...'),
                ],
              );
            }

            if (snapshot.hasError) {
              return Text(snapshot.error.toString());
            }

            if (snapshot.hasData == false) {
              return const Text('No data');
            }

            final temperatureValue = snapshot.data!;
            return Text('Temperature: $temperatureValue');
          },
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Finally, run the app by typing this on the terminal:

flutter run
Enter fullscreen mode Exit fullscreen mode

The app will display a loading indicator for three seconds, then display a value. Let’s go over the code by first analyzing the FutureBuilder widget itself:

FutureBuilder(
  future: ...,
  builder: ...
)
Enter fullscreen mode Exit fullscreen mode

FutureBuilder has two required parameters: future and builder. This might be one of the most straightforward and intuitive widgets in Flutter. The future parameter takes, you guessed it, a value of type Future, and the builder parameter takes a function with two parameters: context and snapshot. In the function we pass to builder, we specify which widgets to display for each state of snapshot. This is what we wrote:

(context, snapshot) {
  if (snapshot.connectionState == ConnectionState.waiting) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      spacing: 16,
      children: [
        const CircularProgressIndicator(),
        const Text('Loading temperature...'),
      ],
    );
  }

  if (snapshot.hasError) {
    return Text(snapshot.error.toString());
  }

  if (snapshot.hasData == false) {
    return const Text('No data');
  }

  final temperatureValue = snapshot.data!;
  return Text('Temperature: $temperatureValue');
}
Enter fullscreen mode Exit fullscreen mode

In the first if statement, we specified what needs to be displayed while the app waits for the value to arrive. In this case, a simple CircularProgressIndicator with a label.

In the second if statement, we defined what must be shown if the app encounters an error while waiting for the value. In this case, we simply chose to display the details of the error with a Text widget.

By the point the app makes it past the first two if statements, having snapshot.hasData return false means the future successfully resolved, but with a null value. In this case, we told the app to display ‘No data’.

Finally, if all goes well, we told the app to display the retrieved value with a Text widget. Now that we went over the builder function, let’s take a look at the functions that return Future:

Future<int> _getTemperature() async {
  await Future.delayed(Duration(seconds: 3)); // Dummy delay of three seconds.
  return 25;
}

Future<int> _getTemperatureError() async {
  await Future.delayed(Duration(seconds: 3));
  throw Exception('An error occurred while retrieving the temperature value.');
}

Future<int?> _getTemperatureEmpty() async {
  await Future.delayed(Duration(seconds: 3));
  return null;
}
Enter fullscreen mode Exit fullscreen mode

All three of these functions first wait for three seconds, then behave in various ways to simulate potential outcomes of waiting for a value. The first one is the optimistic scenario: it successfully returns the value. The second throws an error, and the third one returns a null value. Let’s plug these into our FutureBuilder to see how they turn out:

FutureBuilder(
  future: _getTemperatureError(), // Change this
  builder: ...
)
Enter fullscreen mode Exit fullscreen mode

Now perform a hot reload and observe what the app does. It displays a loading indicator for three seconds and then displays an error message, exactly as we defined in the builder function. Now let’s see what the app does when we pass the null-returning function to our FutureBuilder:

FutureBuilder(
  future: _getTemperatureEmpty(), // Change this
  builder: ...
)
Enter fullscreen mode Exit fullscreen mode

After performing a hot reload, you will see that the app displays a loading indicator for three seconds, then displays the text ‘No data’.

This is the basic usage of FutureBuilder. It builds widgets based on the latest snapshot of its interaction with a Future, both before and after the future completes.

Top comments (0)