What is flutter bloc?
Flutter bloc is one of the state management for Flutter applications. You can use it to handle all the possible states of your application in an easy way.
Why Bloc?
Bloc makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable.
Advantages
- Understand easily what’s happening inside your app.
- More structured code, Easily to maintain an test.
- Know and understand every state of your app.
- Work on a single, stable, popular and effective bloc codebase.
How does it work?
When you use flutter bloc you are going to create events to trigger the interactions with the app and then the bloc in charge is going to emit the requested data with a state
Core Concepts of Bloc
-
Streams
💡 **A stream is a sequence of asynchronous data.**In order to use the bloc library, it is critical to have a basic understanding of
Streams
and how they work.If you're unfamiliar with
Streams
just think of a pipe with water flowing through it. The pipe is theStream
and the water is the asynchronous data.We can create a
Stream
in Dart by writing anasync*
(async generator) function.
Stream<int> countStream(int max) async* { for (int i = 0; i < max; i++) { yield i; } }
By marking a function as
async*
we are able to use theyield
keyword and return aStream
of data. In the above example, we are returning aStream
of integers up to themax
integer parameter.Every time we
yield
in anasync*
function we are pushing that piece of data through theStream
.We can consume the above
Stream
in several ways. If we wanted to write a function to return the sum of aStream
of integers it could look something like:
Future<int> sumStream(Stream<int> stream) async { int sum = 0; await for (int value in stream) { sum += value; } return sum; }
By marking the above function as
async
we are able to use theawait
keyword and return aFuture
of integers. In this example, we are awaiting each value in the stream and returning the sum of all integers in the stream.We can put it all together like so:
void main() async { /// Initialize a stream of integers 0-9 Stream<int> stream = countStream(10); /// Compute the sum of the stream of integers int sum = await sumStream(stream); /// Print the sum print(sum); // 45 }
Now that we have a basic understanding of how
Streams
work in Dart we're ready to learn about the core component of the bloc package: aCubit
. -
Cubit
💡 **A `Cubit` is a class which extends `BlocBase` and can be extended to manage any type of state.**A
Cubit
can expose functions which can be invoked to trigger state changes.States are the output of a
Cubit
and represent a part of your application's state. UI components can be notified of states and redraw portions of themselves based on the current state.-
Create A Cubit:
We can create a
CounterCubit
like:
class CounterCubit extends Cubit<int> { CounterCubit() : super(0); }
When creating a
Cubit
, we need to define the type of state which theCubit
will be managing. In the case of theCounterCubit
above, the state can be represented via anint
but in more complex cases it might be necessary to use aclass
instead of a primitive type.The second thing we need to do when creating a
Cubit
is specify the initial state. We can do this by callingsuper
with the value of the initial state. In the snippet above, we are setting the initial state to0
internally but we can also allow theCubit
to be more flexible by accepting an external value:
class CounterCubit extends Cubit<int> { CounterCubit(int initialState) : super(initialState); }
This would allow us to instantiate
CounterCubit
instances with different initial states like:
final cubitA = CounterCubit(0); // state starts at 0 final cubitB = CounterCubit(10); // state starts at 10
-
State Changes:
💡 **Each `Cubit` has the ability to output a new state via `emit`.**class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() => emit(state + 1); }
In the above snippet, the
CounterCubit
is exposing a public method calledincrement
which can be called externally to notify theCounterCubit
to increment its state. Whenincrement
is called, we can access the current state of theCubit
via thestate
getter andemit
a new state by adding 1 to the current state.Note:
The
emit
method is protected, meaning it should only be used inside of aCubit
.
-
-
Bloc
💡 A
Bloc
is a more advanced class which relies onevents
to triggerstate
changes rather than functions.Bloc
also extendsBlocBase
which means it has a similar public API asCubit
. However, rather than calling afunction
on aBloc
and directly emitting a newstate
,Blocs
receiveevents
and convert the incomingevents
into outgoingstates
.-
Create A Bloc:
Creating a
💡 **Events are the input to a Bloc. They are commonly added in response to user interactions such as button presses or lifecycle events like page loads.**Bloc
is similar to creating aCubit
except in addition to defining the state that we'll be managing, we must also define the event that theBloc
will be able to process.abstract class CounterEvent {} class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0); }
Just like when creating the
CounterCubit
, we must specify an initial state by passing it to the superclass viasuper
. -
State Changes:
Bloc
requires us to register event handlers via theon<Event>
API, as opposed to functions inCubit
.An event handler is responsible for converting any incoming events into zero or more outgoing states.
💡 **Tip**: an **`EventHandler`** has access to the added event as well as an **`Emitter`** which can be used to emit zero or more states in response to the incoming event.abstract class CounterEvent {} class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<CounterIncrementPressed>((event, emit) { // handle incoming `CounterIncrementPressed` event }) } }
We can then update the
EventHandler
to handle theCounterIncrementPressed
event:
abstract class CounterEvent {} class CounterIncrementPressed extends CounterEvent {} class CounterBloc extends Bloc<CounterEvent, int> { CounterBloc() : super(0) { on<CounterIncrementPressed>((event, emit) { emit(state + 1); }); } }
In the above snippet, we have registered an
EventHandler
to manage allCounterIncrementPressed
events. For each incomingCounterIncrementPressed
event we can access the current state of the bloc via thestate
getter andemit(state + 1)
.
-
Bloc vs Cubit
-
Cubit
Cubit is a subset of the BLoC Pattern package that does not rely on events and instead uses methods to emit new states.
So, we can use Cubit for simple states, and as needed we can use the Bloc.
There are many advantages of choosing Cubit over Bloc. The two main benefits are:
Cubit is a subset of Bloc; so, it reduces complexity. Cubit eliminates the event classes. Cubit uses emit rather than yield to emit state. Since emit works synchronously, you can ensure that the state is updated in the next line.
note:
• We don’t have streams, so no transformations.
-
Bloc
When we use BLoC, there’s two important things: “Events” and “States”. That means that when we send an “Event” we can receive one or more “States”, and these “States” are sent in a stream
- We have two streams here: Events and States; and that means that all things that we can apply to a Stream we can do it here. Transformations.
- We can do long duration operations here, things like: API calls, database, compressions or some other complex things.
notes:
- If you send a lot of events for one BLoC could be difficult to track if you are not tracking them correctly.
- Sometimes we want to receive this “state” synchronously, but with BLoC this is not possible.
Note:
BLoC uses mapToState and gives you a stream (async*). Cubit uses normal functions and gives you the result immediately or a Future (with async).
Flutter Bloc Concepts
BlocProvider
💡 Is a flutter widget which creates and provides a Bloc to all of its children.- Is also known as as dependency injection.
-
BlocProvider
will provide a single instance of a Bloc to the subtree below it.
// you must add flutter_bloc package dependency in pubspec.yaml
BlocProvider(
create: (BuildContext context) => BlocA(),
child: yourWidget(),
);
// The context in which a specific widget is built
we access it in a subtree like:
BlocProvider.of<BlocA>(context);
// OR
context.read<BlocA>();
-
By default,
BlocProvider
create the Bloc lazilymeaning that the create function will get executed when the Bloc is looked up for via
BlocProvider
of Bloc a contextnote:
To override this behavior and force the
BlocProvider
create function to be run immediately as soon as possible laze can be set to false inside the widget’s parameters like:
BlocProvider( create: (BuildContext context) => BlocA(), laze: false, child: yourWidget(), );
BlocProvider
handles the closing part of Bloc automatically so that where won’t be any stream leaks all over the place
Question
When we push a new screen into the navigation routing feature of flutter is the BlocA
going to be available there too?
No, because there is another context created on the new rout, a context which the BlocProvider
doesn’t know yet.
so in order to pass the only instance of the Bloc to the new rout, we will use BlocProvider.value()
BlocProvider.value(
value: BlocProvider.of<BlocA>(context),
child: newChild(),
);
Example:
build counter app with bloc
// in this app we use Cubit
// 1- create app states -> counter_states.dart
class CounterState{
int counterValue;
CounterState({
required this.counterValue,
});
}
// 2- create cubit -> counter_cubit.dart
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState(counterValue:0));
void increment() => emit(CounterState(counterValue:state.counterValue + 1));
void decrement() => emit(CounterState(counterValue:state.counterValue - 1));
}
// Ui
class CounterApp extends StatelessWidget {
const CounterApp({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => CounterCubit(), // -> create counter cubit
child: MaterialApp(
body:counterScreen(),
),
);
}
}
//in counter screen you access increment and decrement methods in buttons like:
FloatingActionButton(
child: const Icon(Icons.add),
onPressed: () => context.read<CounterCubit>().increment(),//access increment
),
FloatingActionButton(
child: const Icon(Icons.remove),
onPressed: () => context.read<CounterCubit>().decrement(),//access decrement
),
The Question now is How do we receive the new state inside the UI ?, How do we rebuild specific text widget which prints out the counter value ?
It is now time to introduce the second important Flutter Bloc concept which is BlobBuilder
BlocBuilder
💡 A widget that helps Re-Building the UI based on Bloc state changes
Re-Building a large chunk of the UI required a lot of time to compute, good practice would be to wrap the exact part of the UI you want to rebuild inside BlocBuilder
Ex:
If you have a text widget which updates from a sequence of emitted states and that text is inside a tone of Columns, Rows and other widgets , it is a huge mistake to rebuild all of them just to update the text widget
to rebuild only the text widget → wrap it inside BlocBuilder
-
BlocBuilder
is a widget which requires a Bloc or Cubit and the builder functionbuilder function → will potentially be called many time due to how flutter engine works behind the scenes and should be a pure function that returns a widget in response to a state.
Pure Function → is a function return value depends only on the function’s arguments and no others.
So in this case our builder function should return a widget which only depend on the context and state parameters .
BlocBuilder<BlocA,BlocAStates>( builder: (context, state){ return widget; } )
If the Bloc or Cubit is not provided the
BlocBuilder
will automatically perform lookup for its instance usingBlocProvider
and the current build context.-
you can use
buildWhen
parameter this should take in a function having the previous state and current state as parametersEx:
If text value in current state greater than the text value in previous state you can
return false
so that the Builder function won’t trigger and won’t rebuild UI
BlocBuilder<BlocA,BlocAStates>( builder: (context, state){ return widget; } buildWhen: (previous, current){} )
-
BlocListener
has mainly the same structure as BlocBuilder
but it is really deferent in many ways
💡 Is a flutter widget which listens to any sate change as BlocBuilder does but instead of rebuilding a widget as BlocBuilder did with the builder function it takes a simple void function called listener which is called only once per state not including the initial state.
There is also a listenWhen
parameter function you can pass to tell the BlocListener
when to call the listener function or not as build when parameter was for the BlocBuilder
widget.
BlocListener<BlocA,BlocAStates>(
listener: (context, state){
debugPrint("do stuff have on BlocA's state")
},
child: widget,
)
🔄 In our Counter App:
We need to Re-Build text widget and print increment when click add button and decrement when click remove button, Let’s do this
// rebuild text widget
BlocBuilder<CounterCubit, CounterState>(
builder: (context, state) {
return Text(
state.counterValue.toString(), // initial value = 0
style: Theme.of(context).textTheme.headlineLarge,
);
},
),
// to print in listener we need ro refactor CounterState and CounterCubit
// Let's do this
// refactor CounterState to add wasIncremented variable
class CounterState {
int counterValue;
bool? wasIncremented;
CounterState({
required this.counterValue,
this.wasIncremented,
});
}
// refactor Cubit to change wasIncremented variable value on emit state
class CounterCubit extends Cubit<CounterState> {
CounterCubit() : super(CounterState(counterValue: 0));
void increment() => emit(
CounterState(
counterValue: state.counterValue + 1,
wasIncremented: true, // when click add button
),
);
void decrement() => emit(
CounterState(
counterValue: state.counterValue - 1,
wasIncremented: false, // when click remove button
),
);
}
// Now you can access this variable when listen to state change
Scaffold(
body: BlocListener<CounterCubit, CounterState>(
listener: (context, state) {
if (state.wasIncremented == true) {
debugPrint('Incremented');
} else {
debugPrint('Decremented');
}
},
child: CounterDesign(),
);
// Now We are done
// Now you have created your first Flutter App with Bloc
Perhaps you are in our situation right now when you are updating UI using BlocBuilder
and also print something using BlocListener
isn’t there an easier way to do it of course there is, it is called BlocConsumer
BlocConsumer
💡 Is a widget which combines both BlocListener
and BlocBuilder
into a single widget
BlocSelector
💡 Is a Flutter widget which is analogous to BlocBuilder
but allows developers to filter updates by selecting a new value based on the current bloc state. Unnecessary builds are prevented if the selected value does not change. The selected value must be immutable in order for BlocSelector
to accurately determine whether builder
should be called again.
If the bloc
parameter is omitted, BlocSelector
will automatically perform a lookup using BlocProvider
and the current BuildContext
.
BlocSelector<BlocA, BlocAState, SelectedState>(
selector: (state) {
// return selected state based on the provided state.
},
builder: (context, state) {
// return widget here based on the selected state.
},
)
MultiBlocProvider
💡 Is a Flutter widget that merges multiple BlocProvider
widgets into one.
MultiBlocProvider
improves the readability and eliminates the need to nest multiple BlocProviders
.
Example without it:
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
child: BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
child: BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
child: ChildA(),
)
)
)
Example with it:
MultiBlocProvider(
providers: [
BlocProvider<BlocA>(
create: (BuildContext context) => BlocA(),
),
BlocProvider<BlocB>(
create: (BuildContext context) => BlocB(),
),
BlocProvider<BlocC>(
create: (BuildContext context) => BlocC(),
),
],
child: ChildA(),
)
MultiBlocListener
💡 Is a Flutter widget that merges multiple BlocListener
widgets into one
MultiBlocListener
improves the readability and eliminates the need to nest multiple BlocListeners
.
// without it
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
child: BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
child: BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
child: ChildA(),
),
),
)
→ with MultiBlocListener
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
)
RepositoryProvider
💡 Is a Flutter widget which provides a repository to its children via RepositoryProvider.of(context)
The same exact widget as a BlocProvider
the only difference being that it provides a single repository class instance instead of a single Bloc Instance.
⇒ It is used as a dependency injection (DI) widget so that a single instance of a repository can be provided to multiple widgets within a subtree
NOTE:
BlocProvider
should be used to provide blocs whereas RepositoryProvider
should only be used for repositories.
RepositoryProvider(
create: (context) => RepositoryA(),
child: ChildA(),
);
then from ChildA
we can retrieve the Repository
instance with:
// with extensions
context.read<RepositoryA>();
// without extensions
RepositoryProvider.of<RepositoryA>(context)
MultiRepositoryProvider
💡 is a Flutter widget that merges multiple RepositoryProvider
widgets into one.
⇒ MultiRepositoryProvider
improves the readability and eliminates the need to nest multiple RepositoryProvider
.
By using MultiRepositoryProvider
we can go from:
RepositoryProvider<RepositoryA>(
create: (context) => RepositoryA(),
child: RepositoryProvider<RepositoryB>(
create: (context) => RepositoryB(),
child: RepositoryProvider<RepositoryC>(
create: (context) => RepositoryC(),
child: ChildA(),
)
)
)
To:
MultiRepositoryProvider(
providers: [
RepositoryProvider<RepositoryA>(
create: (context) => RepositoryA(),
),
RepositoryProvider<RepositoryB>(
create: (context) => RepositoryB(),
),
RepositoryProvider<RepositoryC>(
create: (context) => RepositoryC(),
),
],
child: ChildA(),
)
BlocObserver
One added bonus of using the bloc library is that we can have access to all Changes
in one place. Even though in this application we only have one Cubit
, it's fairly common in larger applications to have many Cubits
managing different parts of the application's state.
If we want to be able to do something in response to all Changes
we can simply create our own BlocObserver
.
class SimpleBlocObserver extends BlocObserver {
@override
void onChange(BlocBase bloc, Change change) {
super.onChange(bloc, change);
print('${bloc.runtimeType} $change');
}
}
In order to use the SimpleBlocObserver
, we just need to tweak the main
function:
void main() {
Bloc.observer = SimpleBlocObserver();
CounterCubit()
..increment()
..close();
}
The above snippet would then output:
Change { currentState: 0, nextState: 1 }
CounterCubit Change { currentState: 0, nextState: 1 }
You can observe a Cubit or Bloc Like:
// ObserveACubit
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
}
// ObserveABloc
abstract class CounterEvent {}
class CounterIncrementPressed extends CounterEvent {}
class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) => emit(state + 1));
}
@override
void onChange(Change<int> change) {
super.onChange(change);
print(change);
}
Bloc Architecture
First we ask the questions that are on your mind
What is an Architecture?
What is all about?
Why we need an ArchItecture?
Let’s start
We as human species can’t live without a predefined and stable architecture which is mainly our skeleton, imagine having all kinds of different classes, methods, functions, variables all over the place, this is definitely going to result in a total failure.
Now assimilate this to us humans, How would our life be if we only had our organs every one had to choose where to place their heart, their lungs, their kidneys as probably none of these people would have survived in this situation without organized structure and stable skeleton neither would have any of the apps built without a proper architecture, it’s as simple as that.
so think of architecture as being the skeleton, the blueprint, the structure which keep all your code organized stable and easy to test and maintain.
Now Bloc Architecture is simply an architecture which has Bloc as it center of gravity.
So not only is Bloc
a Design Pattern
and State Management library
but it also an Architecture Pattern
What we know for now is that for every interaction an user makes with the app through each UI there should be an event dispatched to the specialized Bloc or Cubit which will process it and will eventually emit a state that is going to rebuild the UI in a way so that the user gets a feedback of what’s going on with the app, The big unknown variable in all this equation is how Bloc processes the event and perhaps retrieves the necessary data to show to the user, Let me put it this for you almost every app now a days retrieves its data from the internet, so in order to link our Bloc based flutter app with the outer data layer, we need to add the data layer into our equation so that for example:
when Bloc receives a fetching event from UI it will request some data from internet, retrieve it as a response, parse it and return the data with a new state to the user.
Let’s talk about layers
We can split this into three separated main layers which not surprisingly will depend one on another, so the UI is mostly seen as a Presentation Layer
, The Bloc/Cubit as Business Logic Layer
and the last but not the least the app’s data as simply the Data Layer
We’re going to start with Data Layer
which is farthest from the user interface and work our way up to the Presentation Layer
Data Layer
💡 has Responsibility of retrieving and also manipulating data from one or more sources whether we’re talking about network, Database, Request or other asynchronous sources.
The Date Layer has been split up inside three important sub-layers :
- Models
- Data Provider
- Repositories
They will also be dependent on one another.
Suppose you want to build your own first app and you don’t know where to actually start programming the app, what is actually the first thing you should code ?
The best way to start your app of course after you visually and technically designed it is by coding your models, but what exactly is a model?
⇒ Let’s start with model
Model
A model is as its name is implying a blueprint to the data your application will work with.
is a nothing more than a class which contains the data the application itself will be dependent on.
Example → weather API
they shouldn’t be identical to the data provider, Why?
think of what is going to happen if you have weather apis in your app as a source to your weather data their apis will set completely different jsons to your app perhaps on of them has the temperature inside a temp name field and the other one a temperature name field then you will need to parse them separately to your universal models, remember that inside many apps might be multiple data sources but it’s recommended to have only one special data model
in which they will parsed.
This means that models should be independent enough to get weather data from many different weather APIs.
So now after you hopefully understood what a model is, it is time to move over to next sub-layer the Data Provider
Data Provider
💡 Data Provider’s responsibility is to provide raw data to its successor the repository sub-layer
⏰ Let’s concentrate on the Data Provider for a moment..
A Data Provider is actually an API for you own app this means that it is going to be a class which contain different methods and these methods will serve as a direct communication way with the data sources
this where all the magic happens, this is where flutter asks for the required data straight from the internet, all your network requests like http.get
, [http.post](http://http.post)
, put
, delete
will go inside take in mind that the return type of these methods won’t to be the type of model you created earlier but rather of the type of row data you received from the data source which for example may be a type of json string
the component in which we’ll have our model class is instantiated as objects is repository
Repository
💡 Is mainly a wrapper around one ore more Data Providers, it’s safe to say that it’s the part of data layer
Bloc communicates with similar to the other data layer sub parts
-
Repositories are also classes which contain dependencies of their respective data providers so for example:
the weather repository will have a dependency on the weather provider with help of which we’ll call the
getRawWeather
method and retrieve the weatherRowData
. Repository is where the model object will be instantiated with the
RowData
from theDataProvider
or raw data which will be model data withfromJson
methods.The Repository is also a perfect place for
Fine-Tuning
the data before giving it as a response to the Bloc, here you can filter it, sort it and do all kinds of last of last moment changes before it will be sent to theBusiness Logic Layer
.
Business Logic Layer
💡 Is where Bloc and Cubit will be created inside your flutter app, its main responsibility is to respond to user input from the Presentation layer with new emitted state.
-
This layer is meant to be the mediator between the beautiful bright and colorful side of the UI and the dark dangerous and unstable side of the Data Layer
The Business Logic Layer is the last layer that can intercept and catch any errors from with in the Data Layer and protect the application from crashing.
Now since this layer is closely related to the data layer especially the repository sub-layer it can depend on one or more repositories to retrieve the data needed to build up the app state.
🟢 Important fact:
You need to know and understand is that Bloc can communicate one with each other, Cubit can do this too, this is really important
So let’s say we have our previous weather Bloc and we also have an InternetBloc which emits states based on weather there is a stable internet connection or not
Supposedly your internet connection dies when you want to know the weather from your location inside the WeatherBloc, you can depend on the InternetBloc and subscribe to its stream of emitted states then react on every internet state emitted by the InternetBloc
So in this case the InternetBloc would have emitted a no internet state down the stream the weather Bloc will listen to the stream and will eventually receive the no internet state, the WeatherBloc can also emit a state in response to this internet state letting the user know that there is no internet connection.
🚫 Don’t Forget :]
⇒ The subscription to the Bloc needs to be closed manually by overriding the close method, We don’t any stream leaks inside our app
🎉 We have arrived at our final layer of the Bloc Architecture the Presentation Layer
Presentation Layer
This layer sums up every thing related to the user inter face like Widgets
, User Input
, Lifecycle events
, animations
and so on and so froth, also its responsibility in our case is to figure out how to render itself based on one or more Bloc State.
⇒ Most application flows will start with perhaps an app start event which triggers the app to fetch some data from the Data Layer
For Example:
When the first screen of the app will be created inside this constructor there will be a WeatherBloc which adds the app started event so that some location based weather will be displayed on the screen.
Great, Now we are finished all app layers 🎉
→ final structure of weather app
Let’s talk about anther important topic in Bloc, which is BlocTesting
Bloc Testing
💡 Bloc was designed to be extremely easy to test.
Why do developers hate testing so much?
The answer is so simple, We hate testing because the application works fine without it, it seems like a reasonable answer right and indeed it is an application can work without testing but doesn’t mean we should disconsider it so badly.
Testing is in so many cases a life saver, it’s the hero nobody talks about on the news
so if you hate testing much, think of it as acute little dog which will be loyal to you and your app all the time, think of it as a layer which will bark whenever something breaks inside your app
Now let me ask you something, have you ever worked on a big project in which wanted to add anew feature and you realize that after 2 or even 3 days of work despite the fact that the feature works perfectly fine it breaks other features in a completely different place inside your app or maybe have you thought if you ever want to delete a piece of code from within your app will this action be destructive or non-destructive to the app itself, How would you check if every thing is ok ?
after wards most of you will say that they will run the app and check every feature to see if every thing works well but i’m sure that some of you won’t even physically test it and live with the idea that nothing will break a conclusion which is fundamentally wrong.
⇒ Keep that in mind.
So what if i tell you that there is something which will run a full checkup of every feature you wrote inside your app so that whenever you refactor your code
you’ll be stress-free without having to worry about if it’s gonna crash or not, you have already guessed that, i’m talking about our body testing.
Now the difference between Pros & Cons → becomes a problem of
But is it worth it, I’ll let answer this question in the following minutes after we understand the structure and practice testing on our beloved Counter App
How could we implement a test for a feature?
Think of it logically, how would you test if something is working or not?
in big words a test is defined by how you will programmatically tell flutter to double check if the output given by a feature is equal to the expected response you planned on receiving an no other
Now let’s switch to the coding part where i’ll show how testing works and after wards how to write tests inside your app.
👉 Let’s do it
Now you need to clone this repo → CounterApp and add bloc_test
& test
dependency
dependencies:
bloc_test: ^9.1.0
test: ^1.22.0
in test folder we’ll have our test files, a little hint i can give you is that every feature of your app need to be tested in the first place inside the test folder should be kind of symmetrical to the features from within tour app, so all you have to crate the folder symmetrically to the lib folder inside the test folder and add _test
to the name of the file.
In this case we’ll have a counter_cubit_test.dart
in which we’ll test out the functionality of our counter feature.
👉 Remember that all the counter feature does is to increment or decrement a counter value based on which button is pressed by the user.
So inside the test file we need to start by create a MainFunction
inside this main function we’ll go right ahed and create a group with the name of CounterCubit
import 'package:test/test.dart';
void main() {
group('CounterCubit', () {
});
}
group → is actually a way of organizing multiple test for a feature.
For Example:
inside our CounterCubit group we’ll write all the necessary test for the counter feature.
In a group you can also share a common setup
and teardown
functions across all the available test
setUp(() {});
tearDown(() {});
What is the purpose of these functions?
-
Inside the
setup
function you can instantiateFor Example:
The objects our tests will be working with, in our case, we’ll instantiate the
CounterCubit
so that access it later on in our testso
setup
is mainly as its name is implying a function which will be called to create and initialize the necessary data tests will work with.on the other hand
teardown
function
void main() {
group('CounterCubit', () {
late CounterCubit counterCubit;
setUp(() {
counterCubit = CounterCubit();
});
tearDown(() {
counterCubit.close();
});
});
}
-
💡 Is a function that will get called after each test is run and if `group` it will apply of course only to the test in that `group` inside of this perhaps we can `close` the created `Cubit`teardown
Function
Now the time has finally come for us to write our first test which is going to be checking the InitialState
of our CounterCubit
to is equal to CounterState
with a CounterValue
of Zero
.
We’re going this by creating a test
function and give it a description which should denote the purpose of it, in this case in going to be “initial state of CounterCubit is CounterState(counterValue:0)”
group('CounterCubit', () {
late CounterCubit counterCubit;
setUp(() {
counterCubit = CounterCubit();
});
tearDown(() {
counterCubit.close();
});
test('initial state of CounterCubit is CounterState(counterValue:0)', () {
});
});
How do we check this?
I told you earlier that the purpose of any test is to check that the output of the feature is actually equal to the expected output nothing else
To do this, all we need is the except
function which will take two main important arguments ( the actual value
returned by our InitialState
and the expected value
which we’re expecting to be received )
So our InitialState
returned when the Cubit
created will be counterCubit.state
and the expected value
should be a CounterState
which has the counterValue: 0
test('initial state of CounterCubit is CounterState(counterValue:0)', () {
expect(counterCubit.state, CounterState(counterValue: 0));
});
We can run this test by typing dart run
test in terminal
//todo
OR rather by clicking the run button next to the group
.
So if we run this test we will surprisingly receive a complete failure with this Message
Expected: <Instance of 'CounterState'>
Actual: <Instance of 'CounterState'>
in which we expected a instance of CounterState
and we actually receive an instance of CounterState
, here you can actually start to understand why testing is such an amazing tool, it has already signaled us a problem with our app.
👉 Remember that the application worked perfectly when we manually tested it, how come that test fails then
since it tell us that both the expected
and actual
output are instance of CounterState
and we know that both should have a Zero
value inside that means that the instances are still different somehow
and that’s due the fact that inside Dart
language two instances of the same exact class aren’t equal even though they are basically identical this is happening because these two instances are stored in a different part of the memory and Dart
compares their location in memory instead of their values hence making them not equal.
You can override this behavior really simple by using a really popular library you may have already heard about → Equatable
in the backend, Equatable
is just a simple tool which overrides the equal operator
and the HashCode
for every class that extends it hence tricking Dart into comparing the instances by value rather than by where they’re placed in the memory.
So if you want the functionality of comparing to instances like we do in our test, Equatable
is the way to go.
Ok, so all we need to do inside our app right now is to add The Equatable
dependency in pubspec.yaml
file
dependencies:
equatable: ^2.0.5
And then extend
the CounterState
class with it
import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int counterValue;
final bool? wasIncremented;
const CounterState({
required this.counterValue,
this.wasIncremented,
});
}
NOTE:
that now we’ll get a warning telling us that need to override the props
of the Equatable
class
👉 props → are just a way of indicating Equatable
which are the fields in our class that we want to be compared while applying the equal operator
so we’re gonna pass both the counterValue
and wasIncremented
attributes inside the props list
import 'package:equatable/equatable.dart';
class CounterState extends Equatable {
final int counterValue;
final bool? wasIncremented;
const CounterState({
required this.counterValue,
this.wasIncremented,
});
@override
List<Object?> get props => [counterValue, wasIncremented]; // <-
}
Now whenever we will compare two CounterStates
, Dart will compare them attributes by attribute in order to see whether they’re equal or not.
so right now if you go back to our test file and run the test, the test should finally pass
since our expected
and actual
CounterState
had the same counterValue
which is Zero
.
Now it is time for us to test out the functionality of the Increment
and Decrement
functions from inside our counter feature because these are the most important right
⇒ for this we’ll use the blocTest
function from inside the bloc_test
dependency.
We’ll use this because we need to test the output as a response to the increment
or decrement
functions.
-
We’ll start with the test for the
increment
functionalitySo firstly we need to describe it properly
“the CounterCubit should emit a CounterState(counterValue:1, wasIncremented:true) when the increment function is called”
👉 To do this, we’ll use the trio parameters of
build
,act
,expect
from insideblocTest
function.
blocTest( 'the CounterCubit should emit a CounterState(counterValue:1,' 'wasIncremented:true) when the increment function is called', build: null, act: null, expect: null, );
-
build
💡 Is a function that will return `current instance` of the `CounterCubit` in order to make it available to the testing processbuild: ()⇒counterCubit,
-
act
💡 Is a function that will take the `cubit` and will return the `action` applied on it.in our case is the
increment
function →act: (cubit)⇒ cubit.increment()
-
expect
💡 Is a function that will return an `iterable list` which will verify if the order of the state and the actual emitted state correspond with the emitted ones and no other.the
counterCubit
emits only a single state in this example we’ll place it inside the list accordinglyexpect: ()⇒ [const CounterState(counterValue: 1, wasIncremented: true)],
blocTest( 'the CounterCubit should emit a ' 'CounterState(counterValue:1, wasIncremented:true)' ' when the increment function is called', build: () => counterCubit, act: (cubit) => cubit.increment(), expect: () => [const CounterState(counterValue: 1, wasIncremented: true)], );
-
-
test
decrement
functionThe same procedure applies also to decrement function, the only difference being that will
act
by calling thedecrement
function and that willexpect
aCounterState
with acounterValue of -1
inside of1
since we subtract 1 from the initialcounterValue
.
blocTest( 'the CounterCubit should emit a ' 'CounterState(counterValue:-1, wasIncremented:false)' ' when the decrement function is called', build: () => counterCubit, act: (cubit) => cubit.decrement(), expect: () => [const CounterState(counterValue: -1, wasIncremented: false)], );
Let’s run all the test now
Every Thing Passed 🎉 🎉
⇒ Now we can know for sure that the CounterCubit
works as it should.
🤔 Think about this scenario for example:
Let’s say you were moved away from this project and another guy comes into place to continue the development, he doesn’t know Dart that well and he think that the Equatable
package is not needed and that application can work just fine without it so he refactors the code, runs the app and indeed the app is still working perfectly but underneath if we run the CounterCubit
test we can see that if there would be apart inside our app comparing two identical states, the output would have been completely different.
this is exactly why app testing shouldn’t be skipped, it may seem like the refactoring works fine but in reality it isn’t.
so i guess now you understood why it’s worth spending the extra time writing tests for user app.
-
Resources
-
playlist
-
Bloc Library (official website) → recommended
-
-
Source Code of projects with Bloc
Top comments (0)