So what is the FutureBuilder Widget?
Simply put, it’s an elegant way for your app ‘to wait’ for an asynchronous operation to complete before it proceeds. In many cases, it’s used to create the ‘home screen’ for the app by building a widget based on the latest ‘condition’ of a specified Future object. Specifically, building and likely then displaying a particular widget depending upon whether the asynchronous operation associated with a specified Future object is completed yet or not. Further, when completed, building and likely then displaying a particular widget depending upon the resulting value represented by that Future object. In the case of the ‘home screen’ scenario, until the Future object’s asynchronous operation is completed, the app will usually display a ‘waiting’ or ‘loading’ screen.
So, what is the FutureBuilder widget? As it happens, the FutureBuilder widget is a StatefulWidget. In this article, we’ll review how it works.
I Like Screenshots. Click For Gists.
As always, I prefer using screenshots over gists to show concepts rather than just show code in my articles. I find them easier to work with frankly. However, you can click or tap on these screenshots to see the code they represent in a gist or in Github. Ironically, it’s better to read this article about mobile development on your computer than on your phone. Besides, we program on our computers — not on our phones. For now.
Learn By Example
We will use an example from the Flutter Cookbook, fetch data from the internet, to demonstrate a FutureBuilder in action. Below is a screenshot of part of that example displaying a StatelessWidget and how its parameter, post, is provided to a FutureBuilder widget as an instantiated Future object.
The FutureBuilder<T> class documentation emphasizes right away that the specified Future must be obtained earlier and already instantiated. It should not be created right with the FutureBuilder — when the FutureBuilder is called and instantiated. Otherwise, that Future object will be instantiated with the FutureBuilder over and over again with every new call to the build() function containing them, in some instances, possibly not allowing the asynchronous operation to complete.
A Future In Generics
Looking above at the first line of the FutureBuilder class itself, we see that ‘generic data types’ are utilized with a capital ‘T’ and <…> notation. It’s used to indicate what ‘type of data’ returned from an asynchronous operation. In the cookbook example, the ‘type of data’ is a class of type, Post. Note, using a FutureBuilder, the type could be any of the built-in types offered by Flutter as well.
How This Goes
In the screenshot of the example above, you can see the FutureBuilder widget is being called as the ‘child’ argument of a Center widget. Let’s do this in ‘slow-mo’ and see what happens next with the FutureBuilder being called at this point. What would normally take milliseconds, we’ll step through.
It’s All in the Init
Remember, the FutureBuilder is a StatefulWidget. Thus, the initState() function of the StatefulWidget’s accompanying State object is soon called. It’s there where things really start rolling. You can see in the screenshot below, an object of type, AsyncSnapshot, is instantiated in the function, initState().
Note what’s passed to the AsyncSnapshot constructor: An enumerated type with the value of, ConnectionState.none, and the FutureBuilder’s initalData parameter value if any. The enumerated type, ConnectionState, is used by both the FutureBuilder widget and the StreamBuilder widget to convey the progress of their respectable asynchronous operations. We’ll talk of it further, but for now, what’s this ‘widget.initialData’ all about?
Give An Init Value
Below, is a screenshot of a different example where the FutureBuilder is supplied an ‘initial data’ value — not a class of type, Post, but a boolean data type, false. It’s that value that is then passed to the AsyncSnapshot constructor in the function, initState(), listed above as, widget.initialData.
In our cookbook example, no such initial value is supplied, and so the initialData parameter value would be set to null. That parameter is, in fact, a property of the class, FutureBuilder. It involves the Generics data type, T, and so is capable, in this case, of storing a class object of type, Post.
However, if such a parameter was passed, the FutureBuilder would likely build a widget based on that value. That widget will be displayed until the associated asynchronous operation completes and provides its resulting value to the specified Future object. Are you following so far? Don’t worry, I’ll walk through this again. We’re going along in ‘slow-mo’, remember?
The Future Has Class
Again, looking back at the cookbook example, the specific Future object is a class of type, Post, and is provided by the method, fetchPost(). This method is called right away in the main() function and supplied as a parameter to the StatelessWidget, MyApp.
In the screenshot below, you can see the method, fetchPost(), returns an object of type, Future<Post>. It’s in this method, where a website is being called.
The website’s been called to fetch data, and that takes time. Hence, the cookbook example has implemented a FutureBuilder — to ‘wait’ for the data fetch to complete. When encountered, the command await will cause the path of execution to retreat out of the method at that point returning an ‘incomplete’ Future object. The app then continues on to call the FutureBuilder which receives this ‘incomplete’ Future object as its specified Future object.
It Starts with a Build
Like any StatefulWidget, when the FutureBuilder is first called its accompanying State object will first run its initState() function. Soon after, the State object’s build() function is called for the first time. The screenshot below displays that build() function.
Note, that the build() function, in turn, calls the StatefulWidget’s ‘builder’ property. The StatefulWidget that is the FutureBuilder. The screenshot below displaying the cookbook example where the FutureBuilder widget is first defined reveals that property to be a named parameter which is assigned an anonymous method. You can see that that method takes in a BuildContext object and an AsyncSnapshot object as parameters.
And so, every time the build() function of this StatefulWidget is run, this anonymous method will run. That further means, with every subsequent call of the State object’s setState() function, this anonymous method will run. Following so far?
Circular Progress At First
And so, with this first build of the widget tree, a ‘loading spinner’ appears in the center of the screen. Now, why is that? Remember, an ‘incomplete’ Future object of type, Post, had been passed to the FutureBuilder at the start of all this, and when the FutureBuilder’s build() function is then executed that anonymous method is run.
As a consequence, the value,
snapshot.hasData,will be set to false. That’s because there was no
initialData provided as a parameter and the Future object is ‘incomplete.’ Subsequently, since there was no error (
snapshot.hasError), the CircularProgressIndicator widget is called.
Null Has No Data
Again, with the first build, the value,
snapshot.hasData, is false and results in a loading spinner displayed in the center of the screen. You can see below that this boolean expression,
snapshot.hasData, comes from a ‘getter’ in the AsyncSnapshot object which checks if the property,
data, is null or not.
Data of Type T
With the use of Generics, the AsyncSnapshot object defined in the FutureBuilder’s initState() function will have the property called, data, defined as a class of type, Post. Again, if there was an initialData value passed to the FutureBuilder, the property, data, would have been set that value. Otherwise, like in this case, the property, data, is set to null with the Future object not yet able to provide a resulting value.
A good practice is to also include in the anonymous method assigned to the parameter, builder, a call to the getter, hasError. It is also found in the AsyncSnapshot object, and also involves checking if a property is null or not. In this case, it’s the property called, error.
So, What Happens Next?
In this cookbook example with its first build, the Future object is incomplete. So what happens next? What happens next depends on that specified Future object itself. After all, it’s yet to complete its asynchronous operation at this point. Remember that method called, _subscribe(), called in the FutureBuilder’s initState() function? It’s there where the real magic happens.
Where the Magic Happens
A screenshot of the _subscribe() method is displayed below. It’s in this method that the then function is defined for the Future object originally specified with the FutureBuilder. And so, when the Future object complete’s its asynchronous operation, the anonymous method defined and passed as the callback function in the then function will be called. In this case, it’s called when data is fetched from a website.
Note, the parameter of this particular callback function is a Generic type you’ve seen before — of type, T. Thus, a parameter of a class type, Post, is passed into the callback function.
Note further, an additional named parameter defining an ‘onError’ callback function is also passed to the then function. It’s called if the Future object complete’s its asynchronous operation with an error. If there’s an error, a class object is supplied as the parameter. In most instances, it will be of the class type, Exception.
The Future Completes
So what happens next? Well, in this example, until that data is fetched from the website there’s a spinner doing its thing in the center of the screen. However, as you see below, when the asynchronous operation does complete, and the data is fetched from the website either successfully or in error, a brand new immutable AsyncSnapshot object is then created with the enumerated type, ConnectionState.done, passed to it.
Of course, because of the call to the setState() function, the FutureBuilder’s build() function will be fired again but now passing the AsyncSnapshot object, _snapshot, with its ‘connection state’ set to ConnectionState.done. Passed again to that anonymous method assigned to ‘builder’ property. See below. With every setState() function call, this will happen.
The Connection State of the Future Flow
Let’s review the flow of control once again. At first build, we know the Future object is not ready yet (we see the spinner in the center of the screen for a time). Hence, if we looked at the snapshot in the ‘builder’ method at that point, we’d find the value of snapshot.connectionState is set to ConnectionState.waiting.
To demonstrate this, there are breakpoints set along the Switch Case statement now inserted in the build() function below. With the first build, it stops where the connection state is currently set at ConnectionState.waiting.
Now, where did the connection state change from ConnectionState.none to ConnectionState.waiting? Well, I’ll show you. Remember, when using StatefulWidgets, a ‘one-time’ call of an initState() function is called by its associated State object. With FutureBuilder widgets, it’s there where an AsyncSnapshot object is first created with the parameter, ConnectionState.none, and then the _subscribe() function is called.
Set We’re Waiting
It’s in the function, _subscribe(), where we see the new connection state. By their very nature, asynchronous operations take time. In most cases, the Future object is still incomplete by the time the _subscribe() function is called. Giving that function time to define the Future object’s then function. Note, after the then function is defined, a brand new immutable AsyncSnapshot object is created replacing the old one and now supplying a connection state set of ‘waiting.’
And so, for this cookbook example, that’s how things will stay for a second or so: the CircularProgressIndicator widget displayed in the center of the screen and an AsyncSnapshot object with a connection state of ‘waiting’. Of course, I’ve changed things a bit with the Switch Case statement now in the build() function. Instead of a spinner displayed in the center of the screen, we simply have the text, ‘ConnectionState.waiting’, in the center of the screen. Regardless, when indeed the asynchronous operation does complete (the data is fetched from a website), things will change.
When It’s Done, It’s Data
In the screenshots below, we see the progression of the asynchronous operation. While waiting, we see a Text widget was called to display the line, ‘ConnectionState.waiting’, in the center of the screen. However, in an instant, the value of the snapshot.connectionState is set: ConnectionState.done. A breakpoint was placed there at the instant.
Note there, the Text widget is not accompanied by a return command. Instead, execution will continue to find the if statement with the boolean expression, snapshot.hasData, is set to true. Therefore, since the property, data, is of class type, Post, its property, title, will supply a JSON string to yet another Text widget that is returned from the builder method and then displayed on the screen. It’s as simple as that.
So What Happened Back There?
When the asynchronous operation completed, the callback function in the Future object’s then function will run. So in the then function for that specific Future object, when the data fetch was finally completed, the parameter, data, was passed to the ‘withData’ constructor to create yet another brand new immutable AsyncSnapshot object.
Being a Generic data type, we know this parameter, data, to be of the class type, Post. Further, the value, ConnectionState.done, is also passed into the constructor. All this within a setState() function, and thus, the build() function will be called again. This time, the property, snapshot.hasData, will be true, allowing the app to finally proceed with the JSON text displayed on the screen.
What Happens In Error?
So, that was great when everything comes together and works. Let’s see what happens when we introduce a malformed URI to the cookbook example. We’ll see how the FutureBuilder widget and its accompanying AsyncSnapshot object reacts when the asynchronous operation fails.
In the screenshot below, we see the error, HTTP 404 Not Found, is assigned to response.statusCode, and instead of a ‘complete’ Future object of the class type, Post, an Exception object with the message, ‘Failed to load post’, is instead instantiated. The asynchronous operation has completed in error.
And so, if and when you yourself develop the ‘asynchronous operation’ in your apps, remember to throw exceptions when appropriate knowing the FutureBuilder widget has the means ‘to catch’ such errors. You now know there’s a then function with its ‘onError’ parameter assigned a method.
So, in the Cookbook example in this case, when the execution comes out of the fetchPost() function in error, we find ourselves in the specified Future object’s then function. The exception object thrown in the screenshot above is now supplied to the ‘onError’ callback function.
Again, with the execution of a setState() function, a brand new AsyncSnaphot object (of type Post) is passed to the FutureBuilder’s build() function. This time, however, supplied with an Object of type, Exception.
Again, the builder() function is called and, consequently, the anonymous method run. There’s ‘no data’ available in the AsyncSnapshot object, snapshot, this time. Instead, the expression, snapshot.hasError, is set to true, and the actual error message, ‘Exception: Failed to load post’, is displayed.
Note, the property, error, in the AsyncSnapshot object, snapshot, is of type Object and not specifically of type, Exception. Now, why is that?
The property, error, in the AsyncSnapshot object, snapshot, is of type Object so you can assign any class type other than, Exception. The class, Object, is the base class of all objects in Dart after all, and so, the developer is free to assign any object they see fit if and when the asynchronous operation fails. It could be an object that extends the class, Exception, for example, or a class they’ve made up themselves. It gives you options. We love options.
That should be enough. Don’t you think? The rest of this article is just gravy.
What’s with this callbackIdentity?
In the _subscribe() function, you’ll notice two if statements involving the following expression, _activeCallbackIdentity == callbackIdentity. They’re just before the two setState() functions. It’s a little trick that involves the use of the base class for all Dart objects called….well….Object. This object is defined near the start of the function and assigned to a final variable: callbackIndentity = Object();
You see, with asynchronous stuff, a lot could happen between the time the _subscribe() function is called and the time the asynchronous operation is finally completed. In other words, by the time the asynchronous operation is completed, there can be circumstances where those if statements could, in fact, be set to false for one reason or another. (i.e. the expression, _activeCallbackIdentity == callbackIdentity, is no longer true.)
For example, this little trick prevents calling the setState() function in a State object that’s already been disposed of (i.e. the FutureBuilder Widget has been terminated). In another instance, it’s to prevent a setState() function from being called right after a brand new specified Future object was instantiated because the FutureBuilder Widget was ‘rebuilt’ for one reason or another causing the property, _activeCallbackIdentity, to be set to null.
During the lifecycle of the mobile app itself, for example, the StatefulWidget that is the FutureBuilder may be rebuilt. This will cause it’s State object’s didUpdateWidget() function to be called taking in a copy of the ‘old’ FutureBuilder widget as a parameter.
In this function, looking at the first if statement, if the specified Future object has also changed in some way the _subscribe() function is called. However, if the _subscribe() function was called before because _activeCallbackIdentity != null, the function, _unsubscribe(), is also called so to set the property, _activeCallbackIdentity, to null. A brand new AsyncSnapshot object is then instantiated with the same data but assigned the connection state, ‘none’ only to be replaced shortly afterward by yet another brand new AsyncSnapshot object with the connection state of ‘waiting’ in the subsequent call to the _subscribe() function.
The Future has been Disposed
Again, the expression, _activeCallbackIdentity == callbackIdentity, is also used because there may be the case the Future object’s asynchronous operation completes, only to find the FutureBuilder widget has been terminated and its State object disposed of. In other words, the expression, _activeCallbackIdentity == callbackIdentity, is false since the property, _activeCallbackIdentity, is set to null.
An Optional Future?
Did you note, in the _subscribe() function, the code is enclosed in one if statement? At first glance, this implies that the FutureBuilder need not provide a Future object at all to its constructor. What it does mean, however, is that the Future object provided can be null at one time or another during the FutureBuilder’s lifecycle.
A Builder is a Must
Looking at the FutureBuilder class we see that, in fact, it’s the property, builder, that must be passed to the constructor. We see the annotation, @required, and an assert statement stresses that fact. By the very nature of named parameters, however, the constructor parameters, future and initialData, are optional. Hence, the property, future, can be null.
No Future No Data No Error
What happens if you didn’t supply a Future object to that cookbook example? Hopefully, after reading this article, you can deduce that a spinner will appear in the center of the screen.
Why No Future?
Again, the Future parameter is not intended to be optional. It’s to allow the passed Future object to be null at some point in time— usually at the start. For example, if the cookbook example app had the FutureBuiler in a State object and not in a StatelessWidget, this implies that the FutureBuilder could be called again and again (the widget rebuilt again and again), and thus allowing a supplied Future object to be null for one reason or another during one of those calls.
If and when it’s null, you already know what will happen. The builder will fire, the Connection State will be, ConnectionState.waiting, and both snapshot.hasData and snapshot.hasError will be false.
It’s a Future with a Then
So, what’s a FutureButilder in a nutshell? Essentially, a FutureBuilder is a Future object assigned a then method while inside a StatefulWidget. The anonymous function defined in the then method fires off setState() functions when the asynchronous operation finally completes successfully or not. This, of course, fires the build() function of the enclosing State object allowing the anonymous function supplied by the FutureBuilder’s named parameter, builder, to run again returning the appropriate widget based on the outcome of the asynchronous operation.