Exploring Streams in Flutter
Hi Folks! I am back with a brand new article. This time we will be talking about Streams in Flutter. What is Stream and how to use them in Flutter? We will be answering this question in today’s article So tighten up your seat belts as we will be going through a stream of discussions to understand the world of Streams in Flutter.
Plan of Action
As streams are little hard to explain in one or two lines. So I will be telling you a very interesting real world story to make you understand what are Streams. Once the story is complete(theory part) we will be building an app for the same(practical part). So that we get a better idea of how Streams actually work.
Prerequisite
To understand this topic I expect the target audience should have a basic practical understanding of how to build an app using Flutter framework and should have a basic understanding about BLoC pattern. If you are unaware of this pattern you can still enjoy the article. For BLoC pattern you can check out my previous articles(PART1 and PART2) where I explain this pattern in depth.
What is a Stream?
As per Flutter docs, “Streams provide an asynchronous sequence of data.” Hmm! it’s not so clear with that one liner. So let me give you a better explanation by telling you a real world story. Trust me it will be yummy 😜.
Pizza House Story
Lovely pizza
Once upon a time there lived a man John who was very good at making pizzas. He made many different varieties of pizzas like Neapolitan pizza, California style pizza, Sushi pizza, Marinara pizza etc. Pizzas made by John were so yummy that people from all over the world visited him to taste the magic that he spelled within the pizza he made. John had a small house where he took orders, baked pizzas and delivered to the costumers. He had one young beautiful daughter Mia (10% of customers visited the house to see her 😛. Just kidding) who helped her father in his business. John followed a very simple yet effective process to make good business:
- Mia took orders from the customers and passed it over to her John.
- John will look at the order and check if the pizza can be baked or not(no ingredients). If he had the ingredients he will bake the pizza and pass the order to collect office.
- Costumers will come to collect office and take their order .If John can’t bake the pizza he will tell put a note for the customer saying that “The pizza you ordered is not available”.
Takeaway from the Story
After going through that interesting story. There are few interesting things that I would like to point out which will help us to understand the complete concept of Streams.
- The house where pizzas are baked. That house is a permanent place or it won’t be created every time when a new customer shows up. The house is created only once and it will take orders and process it.
- Mia’s only responsibility is to take order and pass it to John. She won’t be deciding whether that pizza can be baked or not. So Mia will only pass the order to John to process it.
- John is the decision maker. He will process the order. He will decide if the pizza can be baked or not. Once he is done processing. He will pass the output(pizza or “Not available) to collect office.
- Customers will come and collect their order(pizza or “Not available”) from the collect office.
So in summary there will be a sequential flow of data into the stream and those data in the stream will be processed and sent to output stream. Let’s now convert the above story into a working code. While in that process I will explain you the complete picture of this Stream concept.
Streams in Dart
When we talk about Streams specific to dart. We will be using the async library provided by dart. This library supports asynchronous programming and contains all the classes and methods to create Streams. So without any further discussion let’s start coding our pizza house. Just a min, before implementing anything let me first show you the final product. So that you can connect things while I put down the code here and it will be easy for you to understand my explanations for each line of code.
If you watch the video. There is a menu which consist of 4 Buttons. Each button has a pizza name written on it. If any of the button is clicked, it will place an order of that particular type of pizza. If you click the button the order is taken by Mia and passed it over to John. John will process it and will put the output in the collect office. Customer will come to collect office and collect his order. He might get the ordered pizza(Image and the success message) or “Out of Stock” message if the pizza is not available. Customers can order same pizza multiple times. But if the stock is over for a particular pizza the output will be “Out of Stock”.
I hope you got some idea about the implementation 😄. If not I will give my best to explain you the complete implementation in detail.
Let’s code
- Create a new Flutter project using. If you don’t know how check this.
- Remove all the code from
main.dart
file and paste the below code:
import 'package:flutter/material.dart';
import 'src/app.dart';
void main() => runApp(new MyApp());
-
You must be seeing an error by now at
MyApp()
. Don’t sweat it, we will be fixing it in the next step. -
Create a package named as
src
under thelib
package. Now create a dart fileapp.dart
under thesrc
package. Paste the below code insideapp.dart
file.
import 'package:flutter/material.dart';
import 'pizza_house.dart';
import 'blocs/provider.dart';
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider(
child: MaterialApp(
home: new PizzaHouse(),
),
);
}
}
As I said in my prerequisite section that you should be aware with BLoC pattern. If you marked yourself green there than you understand the use of Provider()
here. If you not able to recap then in simple words it is an InheritedWidget
which provide access to our bloc instance to the whole app. After pasting the above code you must be seeing some errors. Don’t worry, all will be cleared in the upcoming steps.
-
Create a dart file under the
src
package and name it aspizza_house.dart
. Let’s keep it empty as of now. Once I am done explaining you the concept and plan of attack than we will write some code inside it. -
Next, create another package named
blocs
undersrc
package. Create two dart files inside theblocs
package and name them asbloc.dart
,provider.dart
. Paste below code inside theprovider.dart
file and let’s keep thebloc.dart
file empty:
import 'package:flutter/material.dart';
import 'bloc.dart';
export 'bloc.dart';
class Provider extends InheritedWidget {
final bloc = Bloc();
Provider({Key key, Widget child}) : super(key: key, child: child);
bool updateShouldNotify(_) => true;
static Bloc of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(Provider) as Provider).bloc;
}
}
This class is pretty straight forward. Its sole responsible is to provide access of our bloc instance( bloc.dart
) inside our widget tree.
Just in case you wanted to see the complete project structure:
- Now it’s time to implement the
bloc.dart
file. If you see the code below, do not panic. I will explain you each and every line in-depth then it will make sense I bet. So here goes the complete implementation of thebloc.dart
file:
import 'dart:async';
class Bloc {
//Our pizza house final order = StreamController<String>();
//Our collect office Stream<String> get orderOffice => order.stream.transform(validateOrder);
//Pizza house menu and quantity static final _pizzaList = {
"Sushi": 2,
"Neapolitan": 3,
"California-style": 4,
"Marinara": 2
};
//Different pizza images static final _pizzaImages = {
"Sushi": "http://pngimg.com/uploads/pizza/pizza_PNG44077.png",
"Neapolitan": "http://pngimg.com/uploads/pizza/pizza_PNG44078.png",
"California-style": "http://pngimg.com/uploads/pizza/pizza_PNG44081.png",
"Marinara": "http://pngimg.com/uploads/pizza/pizza_PNG44084.png" };
//Validate if pizza can be baked or not. This is John final validateOrder =
StreamTransformer<String, String>.fromHandlers(handleData: (order, sink) {
if (_pizzaList[order] != null) {
//pizza is available if (_pizzaList[order] != 0) {
//pizza can be delivered sink.add(_pizzaImages[order]);
final quantity = _pizzaList[order];
_pizzaList[order] = quantity-1;
} else {
//out of stock sink.addError("Out of stock");
}
} else {
//pizza is not in the menu sink.addError("Pizza not found");
}
});
//This is Mia void orderItem(String pizza) {
order.sink.add(pizza);
}
}
In the very first line itself you must be scratching your head 😜. What the hell is this StreamController? I know it was coming. Let me explain and ease you 😆.
StreamController
In simple words, its our Pizza house. It is responsible for taking the orders, processing it and giving out the output. But what are the methods that make StreamController a complete Pizza house or in other words what are the methods that are responsible for taking orders, processing it and giving output? Here is a small picture to answer the above question:
StreamController demystified
StreamController has two getters one is sink
another is stream
.sink
is Mia here who will take the orders from the customer and pass it to the stream(John and then collect office). In simple words sink
will add data to the stream
of the StreamController. Now let’s talk about stream
.It will pass the data to the outside world(collect office) after doing some processing( John ). Now if you see the bloc.dart
code. The below lines are the sink
and stream
of StreamController:
//Our pizza house final order = StreamController<String>();
//Our collect office Stream<String> get orderOffice => order.stream.transform(validateOrder);
//This is Mia void orderItem(String pizza) {
order.sink.add(pizza);
}
sink
has a method called as add(data)
which will add data to the stream
. Here it is adding the order to the stream
. orderOffice
(collect office) is a getter which will be called in our pizza_house.dart
(yet to be implemented) to give the output to the customer.
In the above code you must be wondering what is that transform()
is for. It’s a method which will call John to process the incoming orders and put the processed output in the stream(collect office). Now let’s discuss about John a bit who is the main heart of the Pizza house. Here John is our validateOrder
in the above code. validateOrder
is a StreamTransformer.
//Validate if pizza can be baked or not. This is John final validateOrder =
StreamTransformer<String, String>.fromHandlers(handleData: (order, sink) {
if (_pizzaList[order] != null) {
//pizza is available if (_pizzaList[order] != 0) {
//pizza can be delivered sink.add(_pizzaImages[order]);
final quantity = _pizzaList[order];
_pizzaList[order] = quantity-1;
} else {
//out of stock sink.addError("Out of stock");
}
} else {
//pizza is not in the menu sink.addError("Pizza not found");
}
});
StreamTransformer
In simple words, it will take the incoming orders from the stream and will check if the order is valid or not(pizza can be baked or not). If the order is valid it will add the output to the stream using the sink.add(successOrder)
(pizza baked successfully) method. Here we are adding the image of the baked pizza in the stream to show the customer that their pizza is ready. If the order is not valid it will add it to the sink.addError(invalidOrder)
telling the customer that “The pizza you order is out of stock”.
Now I hope things are getting connected for you. Before implementing the pizza_house.dart
there are two more things in the bloc.dart
file that need some explanation. Those are these lines of code:
//Pizza house menu and quantity static final _pizzaList = {
"Sushi": 2,
"Neapolitan": 3,
"California-style": 4,
"Marinara": 2
};
//Different pizza images static final _pizzaImages = {
"Sushi": "http://pngimg.com/uploads/pizza/pizza_PNG44077.png",
"Neapolitan": "http://pngimg.com/uploads/pizza/pizza_PNG44078.png",
"California-style": "http://pngimg.com/uploads/pizza/pizza_PNG44081.png",
"Marinara": "http://pngimg.com/uploads/pizza/pizza_PNG44084.png" };
_pizzaList
is our menu which will hold the type of pizzas and the total quantity that can be baked in the house. _pizzaImages
is map which will hold the images of different kind of pizzas that are baked in the pizza house. These images will be shown to the customer making him feel that he has received his order 😜.
Now the final class that we will be implementing i.e pizza_house.dart
file. Here is the code for it:
import 'package:flutter/material.dart';
import 'blocs/provider.dart';
class PizzaHouse extends StatelessWidget {
var pizzaName = "";
@override
Widget build(BuildContext context) {
final _bloc = Provider.of(context);
return Scaffold(
appBar: AppBar(
title: Text("Pizza House"),
),
body: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[menu1(_bloc), menu2(_bloc), orderOffice(_bloc)],
),
),
);
}
menu1(Bloc bloc) {
return Container(
margin: EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Expanded(
child: RaisedButton(
child: Text("Neapolitan"),
onPressed: () {
bloc.orderItem("Neapolitan");
pizzaName = "Neapolitan";
},
),
),
Container(
margin: EdgeInsets.only(left: 2.0, right: 2.0),
),
Expanded(
child: RaisedButton(
child: Text("California-style"),
onPressed: () {
bloc.orderItem("California-style");
pizzaName = "California-style";
},
),
)
],
),
);
}
menu2(Bloc bloc) {
return Container(
margin: EdgeInsets.all(8.0),
child: Row(
children: <Widget>[
Expanded(
child: RaisedButton(
child: Text("Sushi"),
onPressed: () {
bloc.orderItem("Sushi");
pizzaName = "Sushi";
},
),
),
Container(
margin: EdgeInsets.only(left: 2.0, right: 2.0),
),
Expanded(
child: RaisedButton(
child: Text("Marinara"),
onPressed: () {
bloc.orderItem("Marinara");
pizzaName = "Marinara";
},
),
)
],
),
);
}
orderOffice(Bloc bloc) {
return StreamBuilder(
stream: bloc.orderOffice,
builder: (context, AsyncSnapshot<String> snapshot) {
if (snapshot.hasData) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Image.network(
snapshot.data,
fit: BoxFit.fill,
),
Container(
margin: EdgeInsets.only(top: 8.0),
),
Text("Yay! Collect your $pizzaName pizza")
],
);
} else if (snapshot.hasError) {
return Column(
children: <Widget>[
Image.network("http://megatron.co.il/en/wp-content/uploads/sites/2/2017/11/out-of-stock.jpg",
fit: BoxFit.fill),
Text(
snapshot.error,
style: TextStyle(
fontSize: 20.0,
),
),
],
);
}
return Text("No item in collect office");
},
);
}
}
The very first line inside the build()
method is declaration of the bloc instance. Using this instance we can place orders and check the output inside the collect office. To place order of a particular type of pizza. We have implemented the logic of adding that particular pizza to the stream inside the onPressed()
method of each button. Inside the onPressed()
method we are calling the orderItem(pizza)
which in turn calls the sink.add()
method. To show customers what is the output for their order in the collect office we need to make our widgets work with streams. We will be using StreamBuilder
for this .
StreamBuilder
will listen to the stream from the orderOffice()
method and if there is any data available it will return a widget based on the data coming from stream. StreamBuilder has two important parameters. 1) stream
and 2) builder
. stream
take a method as parameter which returns a stream i.e orderOffice
method from the bloc.dart
file. stream
of StreamBuilder will listen for any new data coming in. builder
will take a method which has two parameters i.e context
and snapshot
. snapshot is something like a data provider. It will get data from the stream and provide it so that we can process it and return the right widget to be displayed. As you can see if there is any data inside the snapshot(we can check using hasData
) we will return an Image
and a Text
widget. The Image
is the image of the pizza that was ordered. Every time a pizza is ordered its quantity is reduced(logic is handled inside the transformer). If there are no more pizza left. The output will be “Out of stock”.
Takeaway from the article
Throughout the app I have not used even a single StatefulWidget still I was able to change the state of the app. In other words I made my app reactive using the power of Streams. Streams are very helpful when you want to listen for changes in your data and react according to it. Hope now it makes sense why we should be making our app as reactive as possible using Streams.
Here is the github repository of the same app which we built today.
Pheww!! We did it. Hell yeah it was an amazing journey. I hope you guys enjoyed it too. Let me know how much you liked this article by giving a big loud clap. Did you know you can give a maximum of 50 claps for each article. If you have any doubts regarding Flutter, connect with me at LinkedIn . See you guys next time with another brand new article. Peace out ✌️.
The Flutter Pub is a medium publication to bring you the latest and amazing resources such as articles, videos, codes, podcasts etc. about this great technology to teach you how to build beautiful apps with it. You can find us on Facebook, Twitter, and Medium or learn more about us here. We’d love to connect! And if you are a writer interested in writing for us, then you can do so through these guidelines.