In the context of Flutter, state management is the process of controlling and managing the changes made to an app’s data and information. These changes which may later influence the appearance and behavior of the UI. Good state management ensures that when a change occurs in one part of the app, it accurately reflects in the other relevant parts of the UI.
Flutter offers quite a number of state management solutions to accommodate different project complexities and developer preference, such as BLoC, Riverpod, Redux, GetX, Provider, etc. But in this article we will be focusing on BLoC (Business Logic Component)
Introduction to BLoC
BLoC which stands for Business Logic Component is a state management pattern that separates the UI from the business logic making the code more maintainable and testable. Business Logic is the set of rules/functionality of an app, it tells the app how to process data, make decisions and interact with other systems.
Notable Components of the BLoC pattern
BLoC: This is the main part of the app, it controls the business logic and manages the app state.
Event: The bloc listens for triggers from the
event
and it responds according to the request of the event. (e.g pressing a button to change the theme of an app to dark, the event sends the message/trigger to bloc then bloc changes the theme)State: When bloc receives an event, it updates the state of the app. This change in state tells the app to update its screen to show the new information.
UI: The UI (User Interface) listens to bloc stream of states and updates itself according to the bloc change of state.
Installation
flutter pub add flutter_bloc
Import
import 'package:flutter_bloc/flutter_bloc.dart';
Concepts of BLoC
Now that we know what bloc is, and how to install and import it in our projects let’s look at it’s core concepts:
Streams: The stream is a sequence of asynchronous data (line of delayed data). It helps send data through the parts of the app over time. Whenever a new state is added to the stream, the UI is notified and updates itself to reflect the new information.
Here’s a simple program using streams:
import 'dart:async'; import 'package:flutter/material.dart'; class TimerPage extends StatefulWidget { @override _TimerPageState createState() => _TimerPageState(); } class _TimerPageState extends State<TimerPage> { late StreamController<int> _controller; late StreamSubscription _subscription; @override void initState() { super.initState(); _controller = StreamController<int>(); _subscription = _controller.stream.listen((seconds) { setState(() { }); }); startTimer(10); } void startTimer(int durationInSeconds) async { for (int i = 0; i < durationInSeconds; i++) { await Future.delayed(const Duration(seconds: 1)); _controller.sink.add(i + 1); } _controller.close(); } @override void dispose() { _subscription.cancel(); _controller.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Timer')), body: Center( child: StreamBuilder<int>( stream: _controller.stream, initialData: 0, builder: (context, snapshot) { return Text('Elapsed seconds: ${snapshot.data}'); }, ), ), ); } }
That code sets up a
StreamController
to manage a stream of seconds. ThestartTimer
function adds seconds to the stream at one-second intervals. TheStreamBuilder
listens to this stream and updates the UI with the latest second value. This creates a simple timer that displays elapsed seconds.Cubit: This is the core component of the bloc package. When an event is triggered the message is sent to the cubit which then changes the state of the app according to the request of the event.
Here’s a simple program using cubit:
import 'package:flutter_bloc/flutter_bloc.dart'; class CounterCubit extends Cubit<int> { CounterCubit() : super(0); void increment() { emit(state + 1); } }
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class CounterPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('Counter')), body: Center( child: BlocBuilder<CounterCubit, int>( builder: (context, count) { return Text('You have pushed the button $count times.'); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () { context.read<CounterCubit>().increment(); }, child: const Icon(Icons.add), ), ); } }
The provided code demonstrates a basic implementation of a Cubit in Flutter. A
CounterCubit
is created to manage an integer state, initially set to 0. Theincrement()
method is defined to increase the state by 1. TheBlocBuilder
in the UI listens to the Cubit's state changes and updates the displayed count accordingly. When the floating action button is pressed, theincrement()
method is triggered, leading to a state update and a subsequent UI refresh. This simple example showcases the core concepts of Cubit and its role in managing state within a Flutter app.Bloc: This is a class that requires an
event
trigger to change the app's state. Instead of directly instructing the Bloc to change the state, we send itevent
. The Bloc then processes these events and determines the necessary state changes. Therefore, the Bloc receivesevent
as input.Here’s a simple program with
bloc
class:import 'counter_bloc.dart'; @immutable abstract class CounterEvent {} class CounterIncrementPressed extends CounterEvent {}
import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'counter_event.dart'; class CounterState extends Equatable { final int count; const CounterState({required this.count}); @override List<Object> get props => [count]; } class CounterBloc extends Bloc<CounterEvent, CounterState> { CounterBloc() : super(const CounterState(count: 0)) { on<CounterIncrementPressed>((event, emit) { emit(CounterState(count: state.count + 1)); }); } }
The code defines a
CounterBloc
that manages a counter state. TheCounterEvent
triggers an increment. When theCounterIncrementPressed
event is received, theCounterBloc
emits a new state with an incremented count. TheCounterState
holds the current count value. This demonstrates how to use theCounterBloc
to trigger state changes and observe the updated state.
Why BLoC?
Separation: Bloc helps you separate the UI from the business logic, making your code easier to read and modify
Testability: You can write unit tests for your BLoCs independently of the rest of your app. You can simulate events, check the emitted states, and verify that the BLoC behaves as expected.
Scalability: BLoC can handle complex state management scenarios, making it suitable for large-scale applications means that BLoC is well-equipped to manage intricate state changes in large, complex apps.
Asynchronous Operations: These are tasks that don't block the main thread of execution. This means that BLoC can handle tasks like fetching data from a network or a database without freezing the app's UI.
With this, we've explored the fundamental concepts of BLoC and its role in managing state within Flutter applications. By effectively leveraging BLoC, you can create robust, scalable, and maintainable apps. For a deeper dive into BLoC and its implementation, consider exploring the official documentation and open-source
Conclusion
I hope you found this post useful and learned something new. If you have any questions or suggestions, please leave a comment.
Your feedback will be greatly appreciated and will motivate me to share more content like this in the future. Thank you for reading.
Connect with me on Twitter | LinkedIn | Github
Happy Coding!