A Comprehensive Guide on Flutter State Management: BLoC

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

  1. BLoC: This is the main part of the app, it controls the business logic and manages the app state.

  2. 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)

  3. 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.

  4. 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. The startTimer function adds seconds to the stream at one-second intervals. The StreamBuilder 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. The increment() method is defined to increase the state by 1. The BlocBuilder in the UI listens to the Cubit's state changes and updates the displayed count accordingly. When the floating action button is pressed, the increment() 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 it event. The Bloc then processes these events and determines the necessary state changes. Therefore, the Bloc receives event 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. The CounterEvent triggers an increment. When the CounterIncrementPressed event is received, the CounterBloc emits a new state with an incremented count. The CounterState holds the current count value. This demonstrates how to use the CounterBloc 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!

11
Subscribe to my newsletter

Read articles from Samuel Ejalonibu directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Samuel Ejalonibu
Samuel Ejalonibu