How to use a FutureBuilder

Having used Flutter for a while, you’ve probably come into contact with a Widget called FutureBuilder. Whether you decided to investigate its purpose or not, this article aims for the answers to the questions: “What is a FutureBuilder?” and “When should I use it?” or “When do I have to use it?”

Preface

When it comes to the question “When do I have to use it?” the answer is pretty clear: never. You can perfectly get around using it and still perform every possible programming task in Flutter. One can say that a FutureBuilder is nothing else but a convenience feature for recurring tasks, making their execution easier and with less boilerplate code.

But what are these kinds of recurring tasks, this strange FutureBuilder is useful for? Actually, the name of the Widget already spoils it: whenever you use a Future.

First example

Before we deep-dive into the semantics of it, let’s start with an example. The following code snippets are taken from the official FlutterFire documentation. FlutterFire is Flutter’s official implementation of a Firebase client. For those you haven’t heard or haven’t used Firebase: it’s one of Google’s development platforms. The purpose is to have a backend for your mobile app without having to manage infrastructure like setting up a server with a web server, a database and such things.

In Flutter, before being able to use the Firebase client, it needs to be initialized. This can be done like this:

import 'package:flutter/material.dart';

// Import the firebase_core plugin
import 'package:firebase_core/firebase_core.dart';

void main() {
  runApp(App());
}

class App extends StatefulWidget {
  _AppState createState() => _AppState();
}

class _AppState extends State<App> {
  // Set default `_initialized` and `_error` state to false
  bool _initialized = false;
  bool _error = false;

  // Define an async function to initialize FlutterFire
  void initializeFlutterFire() async {
    try {
      // Wait for Firebase to initialize and set `_initialized` state to true
      await Firebase.initializeApp();
      setState(() {
        _initialized = true;
      });
    } catch(e) {
      // Set `_error` state to true if Firebase initialization fails
      setState(() {
        _error = true;
      });
    }
  }

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

  @override
  Widget build(BuildContext context) {
    // Show error message if initialization failed
    if(_error) {
      return SomethingWentWrong();
    }

    // Show a loader until FlutterFire is initialized
    if (!_initialized) {
      return Loading();
    }

    return MyAwesomeApp();
  }
}

Even if we strip out the imports and other boilerplate, that’s still hell a lot of code for only initializing and reacting on loading and error.

We have two state variables here (_initialized and _error), two setState() calls and also an implemented initState() method. That’s a lot of state management.

Now let’s have a look at the very same functionality implemented using a FutureBuilder:

import 'package:flutter/material.dart';

// Import the firebase_core plugin
import 'package:firebase_core/firebase_core.dart';

void main() {
  runApp(App());
}

class App extends StatelessWidget {
  // Create the initialization Future outside of `build`:
  final Future<FirebaseApp> _initialization = Firebase.initializeApp();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // Initialize FlutterFire:
      future: _initialization,
      builder: (context, snapshot) {
        // Check for errors
        if (snapshot.hasError) {
          return SomethingWentWrong();
        }

        // Once complete, show your application
        if (snapshot.connectionState == ConnectionState.done) {
          return MyAwesomeApp();
        }

        // Otherwise, show something whilst waiting for initialization to complete
        return Loading();
      },
    );
  }
}

Now this looks much cleaner, doesn’t it? Let’s look at what makes it easier to read:

  • With only 35 lines of code, there are 20 lines less than with the setState() approach
  • The hole App widget does not manage any state. Thus, a StatelessWidget was taken
  • This also eliminates the necessity of initState()
  • The coding style is reactive (declaration of how to react on different properties of the snapshot) instead of imperative (explicitly reacting on changes by setting the state)

But where is the state stored that we saw being stored in two variables (_initialized and _error) in the first snippet?

It’s all encapsulated in the snapshot argument of the builder function, which actually of the type AsyncSnapshot<FirebaseApp>.

When the Future throws an error, it will result in the snapshot’s getter hasError evaluating to true. Until the Future returns something, snapshot.connectionState will stay ConnectionState.waiting

Second example

Okay, let’s head over to a more specific example. Initialization of Firebase is okay to illustrate the principle, but how about a concrete screen?

Let’s say we have a login screen and want it to react on the response of the fictional API (which is of course called asynchronously).

It is supposed to look like this:

Flutter FutureBuilder login in progress
Login in progress
Flutter FutureBuilder login success
Login success
Flutter FutureBuilder login error
Login error

Let’s compare how we could solve this with a FutureBuilder vs. StatefulWidget.

class LoginWithFuture extends StatefulWidget {
  @override
  _LoginWithFutureState createState() => _LoginWithFutureState();
}

class _LoginWithFutureState extends State<LoginWithFuture> {
  Future<bool> _loginFuture;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _loginFuture,
      builder: (BuildContext context, AsyncSnapshot<bool> snapshot) {
        return LoginForm(
          isLoading: snapshot.connectionState == ConnectionState.waiting,
          result: snapshot.hasData ? snapshot.data : null,
          onSubmit: (username, password) {
            setState(() {
              _loginFuture = _login(username, password);
              _loginFuture.then(
                (loginSuccessful) {
                  if (loginSuccessful) {
                    DialogBuilder.showSuccessDialog(context);
                  }
                },
                onError: (error) => print(error.toString())
              );
            });
          },
        );
      },
    );
  }

  Future<bool> _login(String username, String password) async {
    await Future.delayed(
        Duration(seconds: 1)
    );

    return username.toLowerCase() == 'test' && password == '1234';
  }
}

To be fair, in this case we need to use a StatefulWidget for the solution with the FutureBuilder as well. That’s because we use a UI trigger (button tap) to create the Future and talk directly to the “login service” which in this case is just a mock method.

If we used a different state management apporach e. g. BLoC pattern, we would be fine using a StatelessWidget.

The same functionality implemented with setState() looks like this:

class LoginWithSetState extends StatefulWidget {
  @override
  _LoginWithSetStateState createState() => _LoginWithSetStateState();
}

class _LoginWithSetStateState extends State<LoginWithSetState> {
  bool _loading = false;
  bool _success = false;
  bool _error = false;

  @override
  Widget build(BuildContext context) {
    return LoginForm(
      isLoading: _loading,
      result: _success && !_error ? true : null,
      onSubmit: (username, password) {
        setState(() {
          _loading = true;
          _login(username, password).then(
            (loginSuccessful) {
              if (loginSuccessful) {
                _success = true;
                DialogBuilder.showSuccessDialog(context);
              }
            },
            onError: (error) {
              setState(() {
                _error = true;
              });
            }
          );
        });
      },
    );
  }

  Future<bool> _login(String username, String password) async {
    await Future.delayed(
        Duration(seconds: 1)
    );

    return username.toLowerCase() == 'test' && password == '1234';
  }
}

The difference regarding lines of codes is acceptable. However, there are three member variables to be created when using the second approach.

Closing words

In the end it comes down to personal preference when deciding whether to use a FutureBuilder. I can make the code a lot more readable. Also, it removes the necessity to use StatelessWidgets at it stores the state of within the method callback.

When using a pattern in which the state of a certain widget is managed from the outside like it’s the case with the BLoC pattern, it’s more common to have the BLoC call the data layer, await its result and transfer it to the state of the BLoC so that the widget only reacts on the BLoC state.

If you like what you’ve read, feel free to support me:

🥗Buy me a salad

Leave a Comment