Majid Hajian Majid is a Google Developer Expert, an award-winning author, Flutter, PWA, a perf enthusiast, and a passionate software developer with years of developing and architecting complex web and mobile applications.

Async callbacks with Flutter FutureBuilder 

4 min read 1191

Async Callbacks Flutter Futurebuilder

There are many cases where we need to build a widget asynchronously to reflect the correct state of the app or data. A common example is fetching data from a REST endpoint.

In this tutorial, we’ll handle this type of request using Dart and Flutter. Dart is a single-threaded language that leverages event loops to run asynchronous tasks. The build method in Flutter, however, is synchronous.

Let’s get started!

The Dart event loop

Once someone opens an app, many different events occur in no predictable order until the app is closed. Each time an event happens, it enters a queue and waits to be processed. The Dart event loop retrieves the event at the top of the queue, processes it, and triggers a callback until all the events in the queue are completed.

The Future and Stream classes and the async and await keywords in Dart are based on this simple loop, making asynchronous programming possible. In the code snippet below, user input is responding to interaction on a button widget using callbacks:

ElevatedButton(
  child: Text("Hello Team"),
  onPressed: () {
    const url = 'https://majidhajian.com';
    final myFuture = http.get(url);
    myFuture.then((response) {
      // (3)
      if (response.statusCode == 200) {
        print('Success!');
      }
    });
  },
)

ElevatedButton widget

The ElevatedButton widget provides convenient parameters to respond to a button being pressed. As soon as the onPressed event is triggered, it waits in the queue. When the event loop reaches this event, the anonymous function will be executed, and the process continues.

Building Flutter widgets

Now that we’ve learned how asynchronous programming works in Dart, we understand the secret sauce behind Flutter. Now, we can handle the future requests and build our Flutter widgets.

Since the build method in Flutter runs synchronously, we need to find a way to ensure that the app will build widgets based on the data that will be received in the future.

StatefulWidget

One approach is to use StatefulWidget and set the state while information is obtained:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
Future<String> fetchName() async {
  final Uri uri = Uri.https('maijdhajian.com', '/getRandonName');
  final http.Response name = await http.get(uri);
  return jsonDecode(name.body);
 }
class MyFutureWidget extends StatefulWidget {
  @override
  _MyFutureWidgetState createState() => _MyFutureWidgetState();
}
class _MyFutureWidgetState extends State<MyFutureWidget> {
  String? value;
  @override
  void initState() {
    super.initState();

    // fetchName function is a asynchronously to GET http data
    fetchName().then((result) {
      // Once we receive our name we trigger rebuild.
      setState(() {
        value = result;
      });
    });
  }
  @override
  Widget build(BuildContext context) {
    // When value is null show loading indicator.
    if (value == null) {
      return const CircularProgressIndicator();
    }
    return Text('Fetched value: $value');
  }
}

In this example, you may have noticed that we didn’t properly handle possible exceptions, which we can solve by adding an error variable. The process above will work, but we can improve on it.

We made a custom demo for .
No really. Click here to check it out.

FutureBuilder widget

FutureBuilder provides a cleaner, better way to handle future in Flutter. FutureBuilder accepts a future and builds a widget when the data is resolved:

const FutureBuilder({ 
    Key? key, 
    this.future, 
    this.initialData, 
    required this.builder, 
  }) : assert(builder != null), super(key: key);

Let’s take a closer look at how the FutureBuilder widget works:

FutureBuilder<String>(
  future: FUTURE,
  intialData: null, 
  builder: (BuildContext context, AsyncSnapshot<String> snapshot) {

  }
);

The second parameter in the build function is a type of AsyncSnapshot with a specified data type. For example, in the code above, we have defined String.

The snapshot is an immutable representation of the most recent interaction with an asynchronous computation. It has several properties. When an asynchronous computation occurs, it is beneficial to know the state of the current connection, which is possible via snapshot.connectionState.

The connectionState has four usual flows:

  1. none: maybe with some initial data
  2. waiting: asynchronous operation has begun. The data is typically null
  3. active: data is non-null and has the potential to change over time
  4. done: data is non-null

snapshot.data returns the latest data, and snapshot.error returns the newest error object. snapshot.hasData and snapshot.hasError are two handy getters that check whether an error or data have been received.

FutureBuilder is a StatefulWidget that uses state as a snapshot. Looking at the FutureBuilder source code, we can recognize the initial snapshot shown in the code snippet below:

   _snapshot = widget.initialData == null
        ? AsyncSnapshot<T>.nothing()
        : AsyncSnapshot<T>.withData(ConnectionState.none, widget.initialData as T);

We send a future that the widget subscribes to, updating the state based on it:

  void _subscribe() {
    if (widget.future != null) {
      final Object callbackIdentity = Object();
      _activeCallbackIdentity = callbackIdentity;
      widget.future!.then<void>((T data) {
        if (_activeCallbackIdentity == callbackIdentity) {
          setState(() {
            _snapshot = AsyncSnapshot<T>.withData(ConnectionState.done, data);
          });
        }
      }, onError: (Object error, StackTrace stackTrace) {
        if (_activeCallbackIdentity == callbackIdentity) {
          setState(() {
            _snapshot = AsyncSnapshot<T>.withError(ConnectionState.done, error, stackTrace);
          });
        }
      });
      _snapshot = _snapshot.inState(ConnectionState.waiting);
    }
  }

When we dispose the widget, it unsubscribes:

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

void _unsubscribe() {
  _activeCallbackIdentity = null;
}

Let’s refactor our example above to use FutureBuilder:

class MyFutureWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: getName(),
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }

        if (snapshot.hasData) {
          return Text(snapshot.data);
        }

        return Container();
      },
    );
  }
}

Notice that I used the getName() function directly in my FutureBuilder inside the build method.
Every time the FutureBuilder‘s parent is rebuilt, the asynchronous task will be restarted, which is not good practice.

Solve this problem by moving the future to be obtained as early as possible – for example, during initState on a StatefulWidget:

class MyFutureWidget extends StatefulWidget {
  @override
  _MyFutureWidgetState createState() => _MyFutureWidgetState();
}

class _MyFutureWidgetState extends State<MyFutureWidget> {
  Future<String> _dataFuture;

  @override
  void initState() {
    super.initState();
    _dataFuture = getName();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _dataFuture,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }

        if (snapshot.hasData) {
          return Text(snapshot.data);
        }

        if (snapshot.hasError) {
          return Text('There is something wrong!');
        }

        return SizedBox();
      },
    );
  }
}

initState() is called every time the widget is created. Therefore, the getName future function will be memoized in a variable. While my widget can change the state and rebuild each time, my data will remain intact.

StreamBuilder widget

It’s worth also taking a look at StreamBuilder, another widget that handles stream. StreamBuilder and FutureBuilder are nearly identical. However, StreamBuilder delivers data periodically, so you have to listen to it more frequently than FutureBuilder, which you must listen to only once.

The StreamBuilder widget automatically subscribes and unsubscribes from the stream. When disposing a widget, you don’t have to worry about unsubscribing, which could cause a memory leak:

@override
  Widget build(BuildContext context) {
    return StreamBuilder<String>(
      stream: dataStream,
      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {

      },
    );
  }

Conclusion

In this tutorial, you’ve learned how to perform asynchronous callbacks in Flutter to fetch data from a REST endpoint. Asynchronous programming is a powerful force that saves developers’ time and energy. Flutter provides unique tools that further simplify the process.

Building widgets with FutureBuilder and StreamBuilder is a serious benefit of using Dart and Flutter for structuring your UI. Hopefully, now you understand how these two widgets work at the fundamental level through the Dart event loop.

: Full visibility into your web apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page apps.

.
Majid Hajian Majid is a Google Developer Expert, an award-winning author, Flutter, PWA, a perf enthusiast, and a passionate software developer with years of developing and architecting complex web and mobile applications.

Leave a Reply