Complete guide to StreamBuilder in Flutter

In this post, I have elaborately explained StreamBuilder in Flutter. We looked traditional usage of Stream and using StreamBuilder.
StreamBuilder in Flutter

In our recent articles, we took a look at asynchronous programming in Flutter using Streams and Futures. We came to the conclusion that across many situations we have to wait for an event to complete, and in those cases, we use asynchronous methods in Flutter. We use Futures when there is only one delayed event, Streams are used when there is a stream of delayed elements.

In this article, we will take a look at how to consume Stream using StreamBuilder in Flutter. We will look at the traditional method of using streams and how StreamBuilder simplifies it for us.

Our Goal πŸ₯…

In this article, we will build an app that fetches images from an API every 5 seconds for 10 times. The stream for the same will look like this:

// Pseudo Code Representation 

Stream<String> getStream() async* {
 int i = 0;
 while ( i < 10){
   String image = await fetchImage();
   await Future.delayed(Duration(seconds: 5);
 
   yield image;
  }
}

Here’s what we want to achieve:

The Data Stream 🌊

We will create our custom Stream using the yield and async* keywords. The Stream logic will be same across the both implementations so it is wise to keep it in another file. Also, the widget for showing the image in both implementations will be the same. For ease of understanding, I will name the file commons.dart and place it in the lib directory directly. When we work on projects it’s advisable to divide the components into folders according to any architecture, MVC, MVVM, etc. But as this project is just for example and is too small I will declare all the files in the lib directory.

lib/commons.dart
You don't need to bother with the Stream implementation, we here are just concerned with how to consume any stream.

The API endpoint we will use is:

https://api.catboys.com/img

This is an example of the Response from the API endpoint:

{
"url": "https://cdn.catboys.com/images/image_73.jpg",
"artist": "CoverDesign1",
"artist_url": "https://www.deviantart.com/coverdesign1",
"source_url": "https://www.deviantart.com/coverdesign1/art/Artiste-Iya-Chen-Render-396950935",
"error": "none"
}

Note, that the API endpoint is of get type and doesn’t require any authentication or any other parameters. Now let’s create the logic to get a Stream of data:

//In: lib/commons.dart

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

// Taking BuildContext to show SnackBar
Stream getDataStream(BuildContext context) async* {
  int i = 1;
  while (i <= 10) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text("Getting new Image $i"),
        duration: const Duration(seconds: 1),
      ),
    );
    http.Response response =
        await http.get(Uri.parse("https://api.catboys.com/img"));

    Map<String, dynamic> map = json.decode(response.body);


    // Return the value received using yield keyword
    yield map['url'] as String;

    // Delay the next yield by 5 seconds
    await Future.delayed(const Duration(seconds: 5));
    i++;
  }
}

Now that we have our stream fixed let’s look at methods to implement it with our UI. Before that let’s add one more method to commons.dart which is responsible to show the image and the URL:

Column getImageView(String data) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Image.network(
        data,
        height: 200,
        width: 200,
      ),
      const SizedBox(height: 20),
      Center(child: Text(data)),
    ],
  );
}

The code simply takes in the ImageURL and displays it as Image and Text. Now let’s see what the traditional implementation looks like.

Traditional Implementation πŸ‘©β€πŸŒΎ

The Traditional way of implementing Streams in Flutter is like this. We have to keep track of the state of the request. Here, we are using 4 variables to maintain state, eg: dataAvailable, loading, currentData, error statement.

The code works fine but there are some issues with this approach:

  • We have to manually maintain the state. i.e Error, Data, Loading
  • Calling setState((){}) again and again.
  • Boilerplate code that would come with each stream implementation
  • Subscribing and Detaching from the Stream manually.

To resolve all these issues Flutter has a widget that consumes Stream internally and manages its states internally. The widget is StreamBuilder and it is used to efficiently consume Streams in Flutter.

StreamBuilder in Flutter πŸ’β€β™‚οΈ

StreamBuilder is a widget that consumes a Stream object and rebuilds based on the latest value received. The sole purpose of StreamBuilder is to reduce the boilerplate involved in using Streams in the Flutter application.

The StreamBuilder widget received 2 arguments, a Stream and a Builder function. The constructor of StreamBuilder is:

const StreamBuilder({
  Key? key,
  this.initialData,
  Stream<T>? stream,
  required this.builder,
})

Below is an example of how to implement StreamBuilder in Flutter:

The most important object in the builder function is the AsyncSnapshot object. The AsyncSnapshot holds some of the important data regarding the connection, data, error, etc. Some of its important fields are:

  1. connectionState
  2. hasData
  3. hasError
  4. data
  5. error

ConnectionState:

The ConnectionState enum can have 4 possible values:

  • none, maybe with some initial data.
  • waiting, indicating that the asynchronous operation has begun, typically with the data being null.
  • active, with data being non-null, the stream has started yielding events.
  • done, the stream has completed all yield and will now have no future events.

In Streams when the ConnectionState is active or done in both cases it means that we received data. That’s why before checking if the snapshot consists of data or error, we first check if the ConnectionState is active or done.

if(snapshot.connectionState == ConnectionState.active || snapshot.connectionState == ConnectionState.done){
  // Check for Data
}

Now let’s see how to implement the above example with StreamBuilder.

StreamBuilder Implementation πŸ‘·β€β™€οΈ

In the StreamBuilder implementation, we can see that we don’t have to manage the state of the Stream. The StreamBuilder internally sends the data to the builder function as an object of the AsyncSnapshot. Now the code is more readable and now we don’t need to explicitly manage the state of the Stream.

The complete code for this post is available on Github.

Conclusion βœ…

In this post, I have elaborately explained StreamBuilder in Flutter. We looked at the implementation difference between the traditional usage and StreamBuilder. I hope now you can easily use StreamBuilder in your future Flutter apps.

If you have any queries you can comment below and I will be happy to help you. If you are new to Flutter check my post on how to install Flutter on windows?

Leave a Reply

Your email address will not be published.

Previous Post
how-to-create-virtual-machines-on-kvm-featured-image

How to Create Virtual Machines on KVM – The Ultimate Guide

Next Post

App Lifecycle in Flutter

Related Posts