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!
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
widgetThe 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.
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.
FutureBuilder
widgetFutureBuilder 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:
none
: maybe with some initial datawaiting
: asynchronous operation has begun. The data is typically nullactive
: data is non-null and has the potential to change over timedone
: data is non-nullsnapshot.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
widgetIt’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) { }, ); }
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]