So what is the StreamBuilder Widget?
It’s a means to respond to an asynchronous procession of data. A StreamBuilder Widget is a StatefulWidget, and so is able to keep a ‘running summary’ and or record and note the ‘latest data item’ from a stream of data. In most cases, the StreamBuilder takes in the latest ‘data event’ (the latest encountered of a data item from the stream) to determine the next widget to be built.
We’re Talking Streams Here
The StreamBuilder is one of two ‘Async widgets’ supplied by Flutter to deal with asynchronous operations. The other is the FutureBuilder widget. In fact, I would recommend reading the article, Decode FutureBuilder, first and then returning to this article. That’s because much of the same approach applied here in the StreamBuilder can be found in how the FutureBuilder widget works, but using the less complicated Future class compared to the Stream class.
Frankly, Streams are a little more involving. As for this article, we’ll reference Streams as they relate to the StreamBuilder Widget only. There’s a lot more to Streams, but that’s for another article altogether.
No Embedded Code?
As always, I prefer using screenshots over gists to show code in my articles. I find them easier to work with, and easier to read. However, you can click/tap on them to see the code as 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 mostly on our computers; not on our phones. For now.
Learn By Example
We will use an example from the Flutter Cookbook, Working with WebSockets, to demonstrate a StreamBuilder in action. In involves sending a text message to a web server that merely returns that text message back — using a class object of type, Stream. Below is a screenshot displaying the ‘heart’ of the Cookbook example. It’s a build() function which contains the TextFormField widget to enter the text message and the StreamBuilder widget, to receive and then display that text message echoed back by the webserver. Both widgets are contained within a Column widget. It’s a very simple interface for this example.
The Base Of The Stream
StreamBuilder is a StatefulWidget. It’s a StatefulWidget but not before extending another class that actually extends the StatefulWidget, StreamBuilderBase<T, AsyncSnapshot<T>>. The screenshot below depicts the class hierarchy involved:
The class, StreamBuilderBase<T, S> extends StatefulWidget and is an abstract class. It has a few abstract functions that must be implemented when the class is extended. One of which is its build() function.
Now, this is curious since, with most StatefulWidgets, it’s their associated State object that defines a build() function and not the StatefuleWidget itself. However, in the case of the StreamBuilderBase class, it’s associated State object refers back to this StatefulWidget’s own build() function.
T Is For Data
Note Generic data types are in play here. Any type of data can be coming from a stream. It could be of any class, any of Dart’s Built-in types or even the special type called, dynamic. As you see in the screenshot below, the StreamBuilder widget using the Generic data type notation (the capital T) to specify the type of data in the Stream. In most cases, you would specify the data type in angle brackets when defining your StreamBuilder.
Note, in the Cookbook example, the data type is inferred and not specified in angle brackets. However, it could have just as easily been specified. In this case, a WebSocket server is involved and could return any data type. Luckily, the Dart language allows for this with the use of the special type, dynamic. And so, a little change to the Cookbook example would work just the same.
The Heart Of The StreamBuilder
Like the widget, FutureBuilder, the StreamBuilder widget involves a function called, _subscribe(), and is the heart of its operation. Today, to understand how the StreamBuilder works, we’ll start there and work our way out of that function explaining how the parts involved fit together every step of the way.
Listen For The Stream
More specifically, listen for the next ‘data event’ in the Stream. The line indicated in the screenshot above is the heart of the StreamBuilder’s asynchronous operation. The listen() function adds a ‘data handler’ to the stream. It’s the first parameter in the listen() function, and it’s a subscription to the stream that remains bound to that stream until unsubscribed. Now, each time a ‘data event’ occurs (i.e. a new data item arrives from the stream) the subscribed data handler is called. Note, this happens to be a Single-Subscription Streams and, unlike Broadcast Streams, can only have one ‘listener’ subscribed to it.
Upon closer inspection of this particular data handler, we see the StatefulWidget itself, widget, (i.e. StreamBuilder<T>) is referenced and supplied with the newly arrived data item of the data type, T, which in this Cookbook example we know is of the special type, dynamic. All enclosed in a setState() function.
Now, because it’s enclosed in a setState() function, we also know the State object’s build() function will then soon be run supplying the instance variable, _summary, to the StatefulWidget’s own build() function. Let’s examine this instance variable or property called, _summary.
Step Back And See The StreamBuilder
To do this, let’s step back a bit and look at what makes up the StreamBuilder Widget once again. We know the StreamBuilder Widget extends the class, StreamBuilderBase<T, AsyncSnapshot<T>>.
The class, StreamBuilderBase<T, S>, extends the class, StatefulWidget, and creates a ‘library private’ State object called, _StreamBuilderBaseState. Abbreviated screenshots of these two classes are displayed below.
Each has instance variables of particular interest. The StatefulWidget defines a Stream property called, stream, of the generic data type, T. While its associated State object defines a property called, _summary, of the generic data type, S.
Stepping back through the class hierarchy, we can see the data type of the ‘interaction summary’ property, _summary, in the case of the class, StreamBuilder, is a class of type, AsyncSnapshot<T>.
Take A Snapshot
And so, with every arrival of a data item from the stream, it is collected as a ‘summary of that data event’ into an immutable class object of type, AsyncSnapshot. This object is also supplied the appropriate ‘connection state’ in the form of an enumerated type called, ConnectionState. And so, as described in the documentation, this AsyncSnapshot object is an immutable representation of the most recent interaction with an asynchronous computation be it a Stream or a Future. Therefore, it’s used with the FutureBuilder widget as well.
From what you see in its ‘library private’ constructor (a const constructor, in fact, that creates a compile-time constant object), the ‘connection state’ and either a data object or an error object must be supplied. Guess what the data type for the data object and the error object are:
It would make sense the property, data, would be of the Generic data type, T. That’s the same type as the StreamBuilder itself: StreamBuilder<T>. The property is to hold the data item when it arrives from the Stream. Note, the property, error, is of type Object — the base object of all objects in the Dart programming language. This allows the developer, if and when an error occurs, to assign ‘any’ object they see fit — be is of the Exception class or otherwise. Gotta love options.
Let’s see how a ‘snapshot’ is generated. Remember, the ‘data handler’ in the form of an anonymous function was defined in a Stream object’s listen() function. As you know, the Stream object is referenced in the StatefulWidget as an instance variable called, stream. So what happens when a ‘data event’ finally occurs and a data item arrives from the Stream? This happens:
We see the instance variable, _summary, (of a generic type, S ) in this Cookbook example is of the class type, AsyncSnapshot<T>, is taken in (along with the arrived data item) by the StatefulWidget function, afterData() and then returned again. So what is this function, afterData()?
Tracing back through the class hierarchy, we find the afterData() function is defined in the abstract class, StreamBuilderBase<T, S>, but implemented in its subclass, StreamBuilder<T>, as follows:
It is there, where we see the ‘snapshot’ is generated taking in the latest data item. Note, the first parameter, current, is essentially ignored. However, a ‘connection state’ of, active, is also passed into the immutable object and then returned to the caller assigning it to that instance variable, _summary.
Looking again at the _subscribe() function, with this Cookbook example, the instance variable, _summary, will be of a type, AsyncSnapshot<dynamic>. It’s been assigned an immutable object from the afterData() function, and all enclosed by the State object’s setState() function. Thus, causing the State object’s build() function to be called soon after.
With that, the new immutable object of type, AsyncSnapshot<dynamic>, is referenced in the instance variable, _summary, and is passed to the build() function found in the corresponding StatefulWidget. Let’s see where it goes from there.
Fill The Build
The abstract class, StreamBuilderBase<T, S>, defines the build() function, but it is implemented by its subclass, StreamBuilder<T>. This is all conveyed in the two screenshots below.
The StreamBuilder Builder
The StreamBuilder class has an instance variable called, builder. It represents a function, and that function is called when the StreamBuilder’s State object calls its build() function which, in turn, calls the StreamBuilder’s build() function (see screenshot above). That function, in turn, calls the function represented by the property, builder. It turns on that this instance variable or property called, builder, is in fact a ‘required’ named parameter found in the StreamBuilder’s const constructor.
You can guess the typedef, AyncWidgetBuilder<T>, for the instance variable, builder, involves a BuildContext object and a AsyncSnapshot object as parameters. Doing so allows developers to then define and pass, in most cases, an anonymous function with such parameters to the named parameter, builder, in the StreamBuilder’s constructor.
We see in our Cookbook example, an anonymous function is indeed defined and passed to the StreamBuilder’s constructor. It returns a Padding Widget with a Text Widget displaying the data item retrieved from the Stream.
Turn Around And Do It Again
Let’s turn around and work our way through the process again. This time, we’ll start in the Cookbook example where the StreamBuilder is defined. We saw that in the screenshot above. It takes in the WebSocket’s stream object and an anonymous function, but how about any ‘initial data?’
The Initial Snapshot
As you see indicated in the screenshot above, there is a named parameter called, initialData. It’s optional and if provided, must be the same data type as that provided by data stream — and if provided will result in an initial ‘snapshot’ object with that value as its data item, and not otherwise null.
You see the StreamBuilder<T> class also includes a function called, initial(), and returns an AsyncSnapshot object taking in that ‘initial data’ value if any (null otherwise) as well as the connection state of ‘none.’ See above.
The First Build
As you know, the StreamBuilder is a subclass of the StatefulWidget widget, and so, when first built, its associated State object will perform a ‘one-time’ call to the function, initState(). Note, the functions in the initState() function. You’ve seen these before.
You now see there where the initial() function involving the initialData named parameter is actually called and creates the initial AsyncSnapshot object into the instance variable, _summary.
It’s also where the function, _subscribe(), is first called implementing the ‘data handler’ for the specified Stream object. See how things are coming together? The StreamBuilder is now ready to respond to any asynchronous data event that may come from the stream.
Data, On Error Or Done
You can see above, there are two additional ‘named parameters’ utilized in the Stream object’s listen() function. Both specify ‘callback functions’ that are called, in the case of the ‘onError’ parameter, if there‘s an error in the stream or, in the case of the ‘onDone’ parameter, when the stream closes and sends a ‘done’ event.
Again, if any of these three callback functions are executed, a brand new immutable object of type, AsyncSnapshot<T>, is created and assigned to the property, _summary. Note, each calls a different functions implemented in the class, StreamBuilder<T>: afterData(), afterError() and afterDone().
You can see each AsyncSnapshot object created is unique depending on the circumstance. The first one, we’ve reviewed before. It calls the static function, withData(), and supplies the data item and a connection state of ‘active.’ Then, of course, a setState() function is soon called causing the build() to run again passing the data item to the StreamBuilder’s ‘builder’ function.
If an error occurred within the stream, an AsyncSnapshot object is created taking in a class object of type, Object. Again, allowing for ‘any sort’ of class be used to describe the error. In most cases, it’s of the Exception class. Then, of course, a setState() function is soon called, etc, etc.
It’s only when the stream is closing and sends the ‘done’ event, that the current AsynSnapshot object is not ignored. As you can see, it’s inState() function is used to create a new AsynSnapshot object keeping whatever data item the current object contained but now with a connection state of ‘done.’ Then, of course, a setState() function is soon called…you get the idea.
We’re Waiting At First
You know, before even the first data item arrives from the stream, there’s one thing that happens. So, let’s review. Again, in the Cookbook example, a stream object was made and passed to the StreamBuilder’s constructor. Also passed to the StreamBuilder’s named parameter, builder, is an anonymous function that fires with every build of the widget in which it itself returns a widget. Ok, so far so good.
Because this is its ‘first build’, the StreamBuilder’s associated State object fires its initState() function. There, an ‘initial’ AsyncSnapshot object is created taking in any initial data item passed to the StreamBuilder’s constructor. In this case, no initial data item was passed and so the AsyncSnapshot property, data, is null. Also, in this ‘first build’, the function, _subscribe(), is called defining a ‘data handler’, an ‘error handler’ and a ‘done handler’ to the specified Stream object. Thus, readying the app to response to any and all ‘events’ that now may occur from the Stream. Are we good so far?
So after all that! After we’ve ‘connected’ to the stream in a sense. Now we’re ready for anything from the Stream. After that, guess what we do?
We call the function, afterConnected(), and create yet another brand new AsyncShapshot object assigning it to the ‘summary’ variable with a connection state of…’waiting.’
We see the function, inState(), again. It’s called in the function, afterConnected(), assigning the initial data value (if any) to the new AsyncSnapshot object but now with the connection state, ‘waiting.’
So What’s Built At First?
So what’s built the first go around? Well, let’s see….uh….nothing.
When execution of this app arrives in the ‘builder’ function for the first time, it’ll find the boolean expression, snapshot.hasData, is false, and so an empty string is supplied to the Text widget.
Again, the snapshot’s connection state is set to, ConnectionState.waiting. In this particular example, nothing comes from the stream until the user enters some text. That text is then ‘echoed’ back to the user by a web server with the use of a Stream object. Finally, it’s then supplied to a Text widget that displays it on a screen.
And so, with the first build, there’s not much to see. Below is a snapshot (as it were) of the class, AsyncSnapshot. You can see the property, hasData, is in fact, a getter. One that merely checks for null in the instance variable, data.
Let’s Run It!
We’ll now run the app, but in this article, quickly step through the process.
What State Are We In?
Again, when you first start up the example, there won’t be much to see. However, below, I did insert a Switch Case statement in the ‘builder’ function and strategically placed a breakpoint where the connection state is currently set at ‘waiting.’ As you see, at the start, the connection state is exactly that.
After falling out of the Switch Case statement, the boolean expression, snapshot.hasData, will be false, and an ‘empty string’ is displayed. And so, that’s how things would stay until we type in the phrase, ‘Hello World!’, and the first data item returns from the Web Server and back through the Stream.
It’s in the ‘data handler’ where the phrase, ‘Hello World!’, arrives back from the Web Server back through a Stream. There, a brand new AsyncSnapshot object is created and will eventually arrive as the parameter, snapshot, into the ‘builder’ function listed below.
We know, upon arrival to the ‘builder’ function, the AsyncSnapshot object’s connection state will be ‘active’ and the expression, snapshot.hasData, will be true. Hence, the phrase, ‘Hello World!’, is then displayed on the screen. Easy.
That should be enough. Don’t you think? The rest of this article is just gravy.
A Null Stream?
Did you notice there’s an if statement surrounding all the code that makes up the function, _subscribe()? Of course, that implies the instance variable or property called, stream, found in the abstract class, StreamBuilderBase, could be null for one reason or another.
Why A Null Stream?
Looking at the StreamBuilder’s constructor, we can readily see that, indeed, the Stream parameter is optional. That is to say, it’s a named parameter, and by the very nature of named parameters, it’s an optional parameter without the ‘@required’ annotation. So, why is it optional?? Note, that’s not the intent. It’s not meant to be optional. However, it does allows it to be null from time to time. In most cases, throughout the lifetime of the app itself.
No Stream No Data
Back at the initState() function, we know an AsyncSnapshot object is created taking in the ‘initial data’ if any and a connection state of ‘none.’ And so, when the build() function first runs, with a null value for a stream, there’s not going to be much to see — particularly in this Cookbook example.
Null Stream Or Not
Note, in the screenshot above, I literally changed the ‘Stream parameter’ to null without fatal consequence. The ‘Hot Reload’ kicked in and rebuilt the example app without error. This implies it’s ok to have a null value coming from the ‘Stream parameter’ from time to time. It allows it to be null; to be a ‘different’ stream throughout the lifetime of the app itself. As a developer, you are able to discern if and when the stream is null or not. The value, connectionState.none, indicates the Stream is null. The value, connectionState.waiting, indicates the Stream is not null. With the ‘initial data’ option, the developer can supply a data item until the Stream does indeed come ‘online’ and no longer set to null.
Change The Stream
By the way, with hot reload ever-present, when I changed the Stream parameter to null, a breakpoint in the State object’s function, didUpdateWidget(), was tripped. Thus, demonstrating that if and when the Stream does change as it’s allowed to do so, the StreamBuilder is made aware of it. And if the Stream changes, there are a few things that must happen.
With the change confirmed in the first if statement above, the second if statement checks if the _subscribe() function was already called creating a StreamSubscription object stored in the instance variable, _subscription.
Know that this StreamSubscription object first created by the listen() function is an active object providing the ‘onData’, ‘onError’ and ‘onDone’ event handlers. It provides many other event handlers as well although not mentioned here as they’re beyond the scope of this article. It can be used to stop listening or to temporarily pause events. And so, it’s important to close (i.e. cancel) such subscriptions when the Stream is closed and no longer in use. In fact, it can lead to memory leaks if such subscriptions are not cancelled. That’s where the function, _unsubscribe(), comes in.
As you see above, the ‘Stream Subscription’ object calls its cancel() function. This cancels the subscription so it no longer receives events. The very reference is then nullified (assigned to null) to, it would seem, immediately encourage garbage collection. Although I’m personally certain if that works...yet.
Unsubscribe When Disposed
Note, it is so critical when everything is done and the app terminates, the subscription is indeed cancelled — and so the function, _unsubscribe(), is also called in another appropriate function: The State object’s dispose() function. When you make your own Streams, remember to cancel them!
The State Of Change
After cancelling the subscription, we’re not finished yet. A brand new AsyncSnapshot object is created to convey the current state of affairs.
It’s at this point in time that the app is considered to be ‘disconnected’ from a Stream. Therefore, that’s then reflected in the connection state assigned to the new AsyncSnapshot object as ‘none.’ The data item, if any, from the previous ‘snapshot’ object is carried over to the new one.
Stream In Error
I grabbed this little bit of sample code to introduce an error into the Cookbook example. I simply pasted it in the Dart file as a high-level function. All it does is return integers, 1 through 3, in a stream until the for loop hits 4. It then intentionally throws an exception.
I then created a new property or instance variable in the Cookbook example’s StatefulWidget calling it, stream. It’s then initialized with a call to this new function resulting in it creating a Stream object. Guess where this is going.
As you see above, I merely went to the StreamBuilder’s named parameter, stream, and inserted this new Stream object. I’ll then run the Cookbook example again. By the way, did you note the ‘return type’ in this new function of the Built-in type, int. The SteamBuilder Widget (without the angled brackets) can infer the stream’s data type allowing it to now accept a list of integers instead of a series of text. It further involves a Web Server that echoes any data type sent to it.
Step By Step To Error
I placed a number of breakpoints in the StreamBuilder’s _subscribe() function allowing us to step through what would only take microseconds when this Cookbook example is again started up. You can see below when literally ‘a stream’ of numbers arrives at the Stream object’s data handler.
We’re In Error
It’s when the new function reaches the integer, 4, that we intentionally throw an error to see what happens. A different sort of event occurs. The ‘onError’ callback function is instead called, and the instance variable, _summary, gets a different sort of AsyncSnapshot object. Its connection state is still set to ‘active’, but supplied to it now is an object of the class type, Exception.
Consequently, the stream then closes. Since StreamBuilder has code for when a stream closes the instance variable, _summary, is instantly replaced again with yet another AsyncSnapshot object. One that still retains the ‘error’ object, but now with a connection state of ‘done.’
Again, this all happens in code enclosed by the State object’s setState() function, and so you know those newly created ‘snapshot’ object will, in turn, be passed to the defined ‘builder’ function.
Has it happens, I introduced a new if statement in that ‘builder’ function. One that checks the boolean expression, snapshot.hasError. If true, it then displays that error on the screen. See above.
Check For Error
It’s always a good practice to check for errors in your ‘builder’ function. How do we check for errors? In most instances, you use the getter, hasError, in the AsyncSnapshot class as we just did in the example app. It checks for the very existence of the ‘error’ object.
So there we are. A rundown on what the StreamBuilder is and how it works. It all comes down to the listen() function assigned to the designated Stream object. With the series of setState() functions included inside the listen() function, the enclosing StatefulWidget that is the StreamBuilder then responses accordingly to every asynchronous event that may arrive from ‘data source’ to the Stream object — be it a data item or an error or a ‘done’ event.
* source code as of April 03, 2019