Bloc: Behind the Scene
Hey there!
We are talking about these things:
Flutter Default Counter App
Stream
StreamController
Emit
StreamListner
Stream dispose
BlocBuilder
StreamBuilder
We are starting with the basics, let’s talk about the Flutter counter app which is the default Flutter application, we see it every time when we create a new project.
Flutter Default Counter App:
Code:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
}
}
Here they were using setState() to change the state:
void _incrementCounter() {
setState(() {
_counter++;
});
}
what happens if we don’t want to use the setState() or any state management package…
The answer is the stream.
Stream
is a sequence of asynchronous events. It's like a pipe that allows you to receive values over time, instead of getting a single value at once. A stream is an object that emits events or data, which can be listened to by one or many subscribers.
Yes, the flutter bloc works on streams to change the states.
So let’s Start:
abstract class Cubit<T> {
T state;
Cubit(this.state);
}
This is an abstract class Cubit
that holds a generic type T
. The state
the property holds the current state of the cubit, and the constructor sets the initial state.
StreamController():
final StreamController<T> _streamController = StreamController<T>();
This creates an StreamController
object that will be used to broadcast updates to the state.
emit():
This method sends a new state to the StreamController
.
void emit(T state) {
_streamController.sink.add(state);
}
_streamController.sink
is used to add new state values to the stream. When a new state value is added to the stream, the StreamBuilder
widget in the UI layer is notified, and it rebuilds the UI to reflect the new state.
In this code, _streamController.sink.add(state)
is called inside the emit()
function of the Cubit
class. The emit()
function first updates the current state
value with the new state
value that is passed as an argument. Then, it adds the new state
value to the stream using the _streamController.sink.add()
method.
By doing so, the BLoC is able to notify the UI layer of any state changes, and update the UI accordingly.
Stream Listener:
Stream<T> get stream => _streamController.stream;
This getter returns the stream of states from the StreamController
.
dispose():
It’s important to dispose of streams to avoid memory leaks and improve performance. When a widget is removed from the widget tree, it should dispose of any streams it’s listening to.
void close() {
_streamController.close();
}
This method is called when the Cubit is no longer needed and closes the StreamController
.
The Abstract class:
abstract class Cubit<T> {
T state;
Cubit(this.state);
final StreamController<T> _streamController = StreamController<T>();
void emit(T state) {
_streamController.sink.add(state);
}
Stream<T> get stream => _streamController.stream;
void close() {
_streamController.close();
}
}
Counter Cubit:
class Counter extends Cubit<int> {
Counter() : super(0);
void increment() {
state++;
emit(state);
}
}
This is a concrete implementation of the Cubit
class which holds an integer state. The increment()
method is used to increment the current state and emit a new state.
BlocBuilder:
class BlocBuilder<B extends Cubit<S>, S> extends StatelessWidget {
const BlocBuilder({super.key, required this.builder, required this.bloc});
This is a generic widget that takes a Cubit
and a builder function. It rebuilds the widget tree when the state changes.
final Widget Function(BuildContext context, S state) builder;
final B bloc;
These are the properties of the BlocBuilder
. The builder
function is called whenever the state changes, and the bloc
is the Cubit
that is being observed.
StreamBuilder:
return StreamBuilder<S>(
stream: bloc.stream,
builder: (context, snapshot) {
return builder(context, snapshot.data ?? bloc.state);
},
);
This is the build method of the BlocBuilder
. It returns a StreamBuilder
widget that rebuilds the widget tree whenever a new state is emitted by the Cubit
. The builder
function is called with the current BuildContext
and the current state of the Cubit
.
BlocBuilder Code:
class BlocBuilder<B extends Cubit<S>, S> extends StatelessWidget {
const BlocBuilder({super.key, required this.builder, required this.bloc});
final Widget Function(BuildContext context, S state) builder;
final B bloc;
@override
Widget build(BuildContext context) {
return StreamBuilder<S>(
stream: bloc.stream,
builder: (context, snapshot) {
return builder(context, snapshot.data ?? bloc.state);
},
);
}
}
MyApp():
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Bloc',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Bloc: Behind the Scene', bloc: Counter(),),
);
}
}
This is the main app widget that creates a MaterialApp
widget. The title
and theme
properties set the title and theme of the app. The home
property is set to MyHomePage
, which is the main screen of the app.
MyHomePage():
class MyHomePage extends StatelessWidget {
const MyHomePage({
super.key,
required this.title,
required this.bloc,
});
final String title;
final Counter bloc;
void _incrementCounter() {
bloc.increment();
}
@override
Widget build(BuildContext context) {
return BlocBuilder<Counter, int>(
bloc: bloc,
builder: (context, state) {
return Scaffold(
appBar: AppBar(
title: Text(title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$state',
style: Theme.of(context).textTheme.headlineMedium,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
),
);
},
);
}
}
This code defines a MyHomePage
widget, which is a StatelessWidget
.
The MyHomePage
widget takes in three arguments: a Key
, a String
titled title
, and a Counter
object called bloc
.
Inside the widget, there is a function _incrementCounter()
that calls the increment()
function of the bloc
object when the user taps on the floating action button.
In the build()
method, there is a BlocBuilder
widget, which takes in two arguments: a Counter
object called bloc
and a builder function. The builder function returns a Scaffold
widget with an AppBar
, a Center
widget containing a Column
widget with a text widget showing the current state of the bloc
, and a FloatingActionButton
.
The BlocBuilder
listens to the stream
of the bloc
object and rebuilds the widget tree whenever the state
of the bloc
changes. The current state
of the bloc
is passed as a parameter to the builder function.
Hope this will be useful for you guys!