Mastering Flutter’s BLoC Pattern: Elevating Event-Driven State Management (Part 1) medium.com/@kumarsiddy/mastering-flutters-bloc-pattern-elevating-event-driven-state-management-part-1e0b871eadfbf Purushotam Kumar 16 May 2023 Purushotam Kumar Is this another Bloc tutorial that will cover the familiar concepts of event, state, and Bloc? Why do we require yet another tutorial to implement the same functionality, where the UI adds the event, the Bloc listens for the event and triggers the state, and the UI listens for state changes? Indeed, we will be performing the same actions, but with an improved methodology. We have come up with an elegant approach to efficiently manage the Bloc’s state. As our app grew at my previous organization, managing an increasing number of states and sharing data among them became cumbersome. We needed a scalable solution to accommodate our growing product needs. Finally, we developed an effective solution. But before talking about the solution, I’ll talk about the basic concept of the bloc very briefly. What is Bloc? 1/15 BLoC stands for Business Logic Components. But what does “business logic” mean? Business logic refers to the core method or logic that calculates the data to be displayed to the user. BLoC serves as a state management tool that isolates these crucial logics into a distinct component from the UI, offering a means to connect the UI with these logics. Why do we do this? It enhances the code reusability, scalability, and testability of the app. Bloc’s Core components Bloc has three important components. Let’s understand it one by one. Basicallyan Event is a straightforward class that is incorporated into the Bloc to communicate any interactions or changes that occur on the UI. The State, also represented as a class, is emitted by the Bloc to signify the completion of work accomplished by the Bloc. It serves as a notification mechanism for the Bloc’s progress. This class assumes responsibility for various aspects, including invoking the business logic, receiving events, executing the appropriate tasks upon event reception, and emitting a new state to indicate the outcome of the task, whether it was successful or encountered a failure. We’ll get to understand more about these concepts, once we’ll jump into our well-known counter app with a little tweak as per our needs. Project Setup When writing this blog, I’m using flutter:3.7.12 and flutter_bloc: 8.1.2 Let’s first add flutter_bloc plugin inside pubspec.yaml file. 2/15 name:flutter_bloc_example description:AnewFlutterproject. publish_to:'none' version:1.0.0+1 environment: sdk:'>=2.19.6 <3.0.0' dependencies: flutter: sdk:flutter cupertino_icons:^1.0.2 # Flutter bloc package flutter_bloc:^8.1.2 dev_dependencies: flutter_test: sdk:flutter flutter_lints:^2.0.0 Let’s set up our project first. Let’s see the project structure I'm following. You can choose your own structure. I’ve created two folders bloc and ui inside lib folder. bloc will contain all the blocs and ui will have all the screens. Let’s define our main.dart file as following way: void main() { runApp(const MyApp()); } { const MyApp({super.key}); Widget build(BuildContext context) { MaterialApp( theme: ThemeData( primarySwatch: Colors.blue, ), ); }} title: , home: HomePage(), 3/15 Now, Let’s define our HomePage screen. 4/15 import'package:flutter/material.dart'; import'package:flutter_bloc/flutter_bloc.dart'; import'package:flutter_bloc_example/bloc/counter_bloc.dart'; { const CounterPage({super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => CounterBloc(), child: Scaffold( appBar: AppBar( title: const Text('Flutter Bloc'), ), body: _CounterScreen(), ), ); } } { @override Widget build(BuildContext context) { return Center( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'You have pushed the button this many times:', ), Padding( padding: const EdgeInsets.symmetric( vertical: 16, ), child: Text( // This needs to get updated using the new emitted state // We'll come to this later '0 Times', style: Theme.of(context).textTheme.headlineMedium, ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( 5/15 onPressed: () => _onIncrement(context), icon: const Icon( Icons.add_circle, size: 48, color: Colors.blue, ), ), const SizedBox(width: 16), IconButton( onPressed: () => _onDecrement(context), icon: const Icon( Icons.remove_circle, size: 48, color: Colors.blue, ), ), ], ), ], ), ); } void _onIncrement( BuildContext context, ) { // Incrementing by 1 BlocProvider.of<CounterBloc>(context).increment( incrementNumber: 1, ); } _onDecrement( BuildContext context, ) { BlocProvider.of<CounterBloc>(context).decrement( decrementNumber: , ); }} Let’s understand what is happening here. We have wrapped Scaffold with a bloc provider, It will push the CounterBloc’s instance inside the widget tree, so that it can be accessed from inside by any child widget. We can see it in the below code block. BlocProvider( appBar: AppBar( ), ); create: (_) => CounterBloc(), title: Text(), ), child: Scaffold( body: _CounterScreen(), And also, We have defined two buttons here. One to increment and another to decrement. As soon as a button will be tapped, we will try to get the instance CounterBloc that has been pushed by the provider to the tree and will call the corresponding method to increment/decrement the counter. We can see it in the below code block. 6/15 _onIncrement( BuildContext context, ) { (context).increment( incrementNumber: , ); BlocProvider.of<CounterBloc> } Now, Let’s define our counter_event class. part of 'counter_bloc.dart'; abstract {} { finalint incrementNumber; CounterIncrementEvent({ requiredthis.incrementNumber, }); } { finalint decrementNumber; CounterDecrementEvent({ .decrementNumber, });} Here we have defined one abstract class CounterEvent. This is abstract because we don’t want to use it directly. Every other event will extend this class. CounterIncrementEvent and CounterDecrementEvent is extending CounterEvent. Now, Let’s define our counter_state class. 7/15 part of 'counter_bloc.dart'; abstract { final CounterStateStore store; CounterState({ requiredthis.store, }); } { CounterInitialState({ requiredsuper.store, }); } { CounterUpdateState({ requiredsuper.store, }); } @immutable { finalint? counter; const CounterStateStore({ this.counter, }); CounterStateStore copyWith({ counter: counter ?? .counter, ); counter, }} }) { CounterStateStore( Here For state class, we are following the same pattern as the definition of the event. CounterState is abstract. There is a difference in how the CounterState is defined. It includes a final field variable called CounterStateStore. This change is not commonly found in other tutorials. However, this small change has had a significant impact on our code. To understand it better, let’s look at the definition of the CounterStateStore class. It is marked as immutable, meaning you cannot change the value of its variables without creating a new object. CounterStateStore should include variables that should not be lost while changing the 8/15 state. In our case, we are manipulating the value of the counter and we don’t want to lose its value when changing the state. That’s why the counter variable is defined inside the store class. Before discussing the solution we found, there’s another important thing to mention. We have a method called copyWith inside the CounterStateStore. This method is responsible for returning a new object with the previous counter value copied. Later on, in the CounterBloc class, we will utilize this copyWith method. We will continue our discussion about the solution we arrived at in that context. If the CounterStateStore contains a large number of variables, it can become challenging to write and manage the copyWith method. In the second part of this blog, we will explore how we can address this issue using different packages. Now, Let’s define our bloc class. 9/15 import'package:flutter/foundation.dart'; import'package:flutter_bloc/flutter_bloc.dart'; part'counter_event.dart'; part'counter_state.dart'; <, > { CounterBloc() : super( CounterInitialState( store: const CounterStateStore( counter: 0, ), ), ) { on<CounterIncrementEvent>(_onCounterIncrement); on<CounterDecrementEvent>(_onCounterDecrement); } Future<void> _onCounterIncrement( CounterIncrementEvent event, Emitter<CounterState> emit, ) async { final counter = state.store.counter! + event.incrementNumber; emit( CounterUpdateState( store: state.store.copyWith( counter: counter, ), ), ); } Future<void> _onCounterDecrement( CounterDecrementEvent event, Emitter<CounterState> emit, ) async { final counter = state.store.counter! - event.decrementNumber; emit( CounterUpdateState( store: state.store.copyWith( counter: counter, ), 10/15 ), ); } void increment({ requiredint incrementNumber, }) { add( CounterIncrementEvent( incrementNumber: incrementNumber, ), ); } decrement({ decrementNumber, decrementNumber: decrementNumber, ), }) { ); add( }} CounterDecrementEvent( Let’s begin with the constructor of the bloc. In the constructor, we are initializing the counterBloc with its initial state. This sets the initial data for the bloc. Additionally, we are registering all the event change listeners in this constructor. These event change listeners will be triggered whenever the UI triggers an event. CounterBloc() : ( CounterInitialState( store: CounterStateStore( counter: , ), ), <CounterIncrementEvent>(_onCounterIncrement); <CounterDecrementEvent> (_onCounterDecrement);} ) { Let’s understand the methods defined inside bloc, which will be called as soon as the user will tap on the increment/decrement button on the UI. increment({ incrementNumber, incrementNumber: incrementNumber, }) { ), add( ); } CounterIncrementEvent( Here, UI will pass incrementedNumber and as soon as this method will be called, we’ll add the events to the bloc. And the listener will call the _onCounterIncrement method after adding the event. Let’s understand what’s cooking inside this _onCounterIncrement method. Future<> _onCounterIncrement( CounterIncrementEvent event, Emitter<CounterState> emit, ) { counter = state.store.counter! + event.incrementNumber; emit( CounterUpdateState( store: state.store.copyWith( counter: counter, ), ), ); } This method performs a straightforward task. It increments the counter, sets the incremented value in the store, and emits the new state. Now, We’ll talk about the final solution we arrived at. We have already defined copyWith method inside CounterStateStore, By utilizing this defined method, we are going to have the capability to retain the previous value without passing it in the state. This concise method 11/15 grants us the freedom to streamline state updates efficiently without losing any data or without having the burden to pass all the parameters again. Now, It is time to update our counter_page with a builder around it, which reacts to the state change. 12/15 import'package:flutter/material.dart'; import'package:flutter_bloc/flutter_bloc.dart'; import'package:flutter_bloc_example/bloc/counter_bloc.dart'; classCounterPageextendsStatelessWidget { constCounterPage({super.key}); @override Widgetbuild(BuildContext context) { returnBlocProvider( create: () =>CounterBloc(), child: Scaffold( appBar: AppBar( title: constText('Flutter Bloc'), ), body: _CounterScreen(), ), ); } } class_CounterScreenextendsStatelessWidget { @override Widgetbuild(BuildContext context) { // We have added builder here returnBlocBuilder<CounterBloc, CounterState>( // This builder will get called on each state change builder: (context, state) { returnCenter( child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ constText( 'You have pushed the button this many times:', ), Padding( padding: constEdgeInsets.symmetric( vertical: 16, ), child: Text( // Now this text will have the value from the state's store '${state.store.counter ?? 0} Times', style: Theme.of(context).textTheme.headlineMedium, ), ), Row( 13/15 mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: () =>_onIncrement(context), icon: constIcon( Icons.add_circle, size: 48, color: Colors.blue, ), ), constSizedBox(width: 16), IconButton( onPressed: () =>_onDecrement(context), icon: constIcon( Icons.remove_circle, size: 48, color: Colors.blue, ), ), ], ), ], ), ); }, ); } void_onIncrement( BuildContext context, ) { // Incrementing by 1 BlocProvider.of<CounterBloc>(context).increment( incrementNumber: 1, ); } () { .<>(context).( : , ); }} In the above code, UI is encapsulated within a BlocBuilder, utilizing the builder method that is invoked with each state emitted by the Bloc. Voila! As you tap on the buttons, the counter value displayed on the screen promptly updates. Explore the code base of this project by visiting the GitHub repository. Conclusion: This concludes Part 1 of the Bloc tutorial, where we gained a solid understanding of the fundamental concepts of event, Bloc, and state in action. And we also understood how a small change can solve a bigger problem. In Part 2, we will delve deeper into utilizing various 14/15 packages(such as get_it, freezed, dartz, build_runner, and mason) that significantly simplify the state management process using bloc. Thank you for reading this blog, and I hope you found it informative and engaging. Stay tuned for more updates and insights on the ever-evolving world of Dart and Flutter. If I got something wrong? Let me know in the comments. I would love to improve. Clap 👏 If this article helps you. If you require any assistance or consultation for your project, please feel free to email me at purushotam.kr@hotmail.com 15/15