BLoC | Cubit | State Management | Flutter

BLoC | Cubit | State Management | Flutter
  • BLoC (Business Logic Component) is a popular architectural pattern in the Flutter community for building scalable and maintainable apps.
  • In this pattern, the UI communicates with a BLoC, which handles all the business logic and state management. The BLoC then emits streams of data, which the UI can listen to and update the UI accordingly.
  • It can be modified and accessed by the widgets.
  • Bloc is a design pattern created by Google.
  • A state management library called Bloc was created and maintained by Felix Angelo. It helps developers implement the Bloc design pattern in their Flutter application. It means that a developer must know the state of an app at any time. There should be something displayed on the screen for every interaction with the app to let users know what is happening.

BLoC Architecture works?

The Bloc class: This is the main class for creating a BLoC. It handles all the business logic and state management.

  1. Events: Events are simple classes that indicate an event and you trigger them on a certain point like a button press or inside initState. Eg: calling GET API will have an event class FetchNotesEvent.
  2. States: The state is part of the application’s state. It is the output of a Bloc. When a state changes, UI components will get the notification, and based on the current state it can re-render itself.
  3. BLoC: This is a class that maps events to states. When the FetchNotesEvent is triggered we send the FetchNotesLoadingState to the UI. After the API call is finished we send FetchNotesSuccessState or FetchNotesErrorState based on whether the API call was a success or failure.
  4. BlocBuilder: This widget listens to a stream of data emitted by a BLoC and rebuilds the UI when the data changes. BlocBuilder is similar to other builder widgets(StatefulBuilder, StreamBuilder, Builder) you might have used. It gives you access to context and the state. Inside the builder callback, you can check the current state and display widgets accordingly. Signature for the `builder` function which takes the BuildContext and state and is responsible for returning a widget that is to be rendered.
  5. Transition: The change from one state to another is called transition. It holds the current state, event, and the next state.
  6. state variable that we can see, has access to the states. In this case, the state is just an integer value that will get incremented. So we can directly display the state.
  7. Sink (Sending Values to a Stream): It captures user interactions (e.g., button clicks) from the UI and sends these interactions as values to a stream.
  8. Stream: This represents the application data. The stream is managed by the business logic and any changes in the stream trigger UI updates.
Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, state) {
            return Text(
              '$state',
              style: TextStyle(fontSize: 50.0),
            );
          },
        ),
      ),

Bloc Work?

Bloc uses a state and event to manage actions. Think of Bloc as a gamepad and your app, a Soccer video game (or whatever your favorite game is). An event is when you press a button on your gamepad, and a state tells us what button(s) are pressed at a particular instance. When a user adds an event, our bloc emits (sends) a new state to the UI. The UI is then updated to move a player, shoot the ball, or both based on the state of the gamepad.

Bloc: The central class responsible for managing state and business logic.
Events: Actions or user interactions that trigger state changes.
States: Represent the various states of the application.
BlocProvider: A widget for providing BloC instances to the widget tree.

Bloc Create (Bloc Initialization)

  • This component refers to the initialization of your BLoC instances. In your application, you create instances of your BLoC classes to manage different parts of the application’s state and logic.
  • Typically, you create BLoC instances at the top level of your widget hierarchy, often within the main function or the build method of your app’s root widget.

Bloc Builder

The Bloc builder returns a widget based on the state. The state is built in a required builder function.

With the help of the widget, the overall boilerplate code requirement gets reduced. Therefore, the tasks of building and rebuilding the child subtree during state changes become easy to perform.

Re-Builds the UI FOR EVERY NEW STATE coming from the bloc.

Rebuilding Widgets

this builder function may be called MULTIPLE TIMES PER STATE (due to Flutter Engine)

BlocBuilder<BlocA, BlocAState>(
  builder: (context, state) {
    // do stuff here based on BlocA's state
  }
)

Bloc Provider

BlocProvider, you can create new blocs and close the BloC as well, at the same time. This is easy to access from the remaining subtree.

Creates & Provides the ONLY INSTANCE of a bloc to the subtree.

THE ONLY INSTANCE of a bloc to the subtree

BlocProvider(
  create: (BuildContext context) => BlocA(),
  child: ChildA(),
);

Bloc Listener

The bloc listener widget is used as an observer. It requires a listener function and an optional child. It does not build or rebuild UI, but can be used to react to state changes by calling functionalities such as toasts, alert dialogs, or just executing functions.

listener function is called ONLY ONCE PER STATE can not INCLUDING THE INITIAL STATE.

BlocListener<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  child: Container(),
)

Bloc Consumer

This Bloc widget is a combination of the listener and the builder. It uses a required listener and builder function to react to state changes. It is recommended that Bloc consumer be used only when you want to simultaneously rebuild your UI and perform other actions in response to state changes.

OR

Bloc Consumer combines the functionalities of both Bloc Listener and Bloc Builder. It allows you to respond to state changes by executing functions and building widgets simultaneously. This is particularly useful for situations where you need to both update the UI and call functions in response to state changes.

BlocConsumer<BlocA, BlocAState>(
  listener: (context, state) {
    // do stuff here based on BlocA's state
  },
  builder: (context, state){
    // do stuff here based on BlocA's state
  }
)

Bloc Selector

Bloc Selector is a bloc builder that goes through a “filter” to return a bool based on the current state. The builder function uses the returned bool value to build the widget.

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => AppCubit(),
      child: MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: const MyHomePage(),
      ),
    );
  }
}

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(context.read<AppCubit>().state.title),
      ),
      body: Center(
        child: BlocSelector<AppCubit, AppState, int>(
          selector: (state) {
            return state.value;
          },
          builder: (context, value) {
            print("BlocSelector state.value rebuild");
            return Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '$value',
                  style: Theme.of(context).textTheme.headlineMedium,
                ),
                ElevatedButton(
                    onPressed: () {
                      context.read<AppCubit>().changeTitle('Hello World');
                    },
                    child: const Text('Change title'))
              ],
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          context
              .read<AppCubit>()
              .changeCounter(context.read<AppCubit>().state.value + 1);
        },
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}

class AppCubit extends Cubit<AppState> {
  AppCubit() : super(AppState(0));

  changeCounter(int value) {
    emit(AppState(value));
  }

  changeTitle(String title) {
    emit(AppState(state.value, title: title));
  }
}

class AppState {
  final int value;
  final String title;
  AppState(this.value, {this.title = 'Flutter Demo Home Page'});
}

BlocProvider/MultiBlocProvider

BlocProvider is a Bloc widget that provides Blocs to its children. It is typically used at the top of a widget tree to enable all widgets within a subtree to retrieve the bloc using BlocProvider.of<T>(context).

RepositoryProvider/MultiRepositoryProvider

RepositoryProvider is used to provide repositories or. It can be retrieved by its children using RepositoryProvider.of<T>(context).

The MultiRepositoryProvider is used to provide multiple RepositoryProviders. Its usage is similar to the MultiBlocProvider. In this case, you’re providing repositories instead of Blocs to the list of providers.

BlocConsumer, have four arguments let’s go over them one by one,

builder — It rebuilds the widget tree in response to the state changes, which means whenever you emit a new state it will rebuild the widgets so that you can use the value sent in the state that is emitted from the Bloc. So in this example, we have passed a text from the bloc in the UpdateTextStateand used it to so on the UI, This state is emitted whenever the “Tap me!!” button is tapped.

listener — So whenever a state is emitted the listener also gets invoked, but unlike the builder, it doesn’t return any widget. It gets called once per state change, that’s why we generally use a listener that happens once whenever the state changes, like showing a snack bar, dialog, bottom sheet, or navigating to the next screen.

buildWhen — This is an optional parameter that provides previous and current states and returns a boolean, so if we return true it will call the builder, or else it won’t if you don’t use this parameter the builder will be called for each state change.

listenWhen:This is similar to buildWhen but used to control the Listener, if it returns true then the listener gets called.

.when: You must provide individual logic for each bloc state.

.maybeWhen: You must provide logic for some bloc states and logic for defaults.

In short

  • Bloc must extend the base Bloc class from the core bloc package.
  • EachBloc must define an initial state
  • It must implement a function called mapEventToState. This function takes incoming event as the argument. It also returns a stream of new state as output.
  • To access the current bloc state we can use currentState property
  • A bloc has a dispatch method. Dispatch takes an event and triggers mapEventToStateDispatch can be called in the presentation layer or from within a bloc
  • onTransition is called before a bloc state is updated
  • onError can override to know bloc exception.
  • BlocObserver can be used to observe all blocs as well. onCreate, onEvent, onChange, onTransition, onError and onClose.

Cubit

Bloc has a limitation and that is without adding the Event we cannot emit any states. Events are added first and only the State is emitted.

Cubit is the subset of bloc package.

Cubit is lightweight compared to bloc.

Cubit do not have Event Streams and only has State Streams

Events are replaced by functions so simply we need to call the function to get the States. So this reduces the extra boilerplate codes.

While in Cubit in a single function call, we can emit multiple times the States.

Cubit is a special kind of Stream component based on some functions called from the UI. Functions that rebuild the UI by emitting different states on a Stream.

part of 'home_cubit.dart';

@freezed
class HomeState with _$HomeState {
  const factory HomeState({
    @Default(HomeCubit.defaultTabIndex) int tabIndex,
  }) = _HomeState;
}

import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:injectable/injectable.dart';

part 'home_cubit.freezed.dart';
part 'home_state.dart';

@lazySingleton
class HomeCubit extends Cubit<HomeState> {
  HomeCubit() : super(const HomeState());

  static const defaultTabIndex = 0;

  void changeTab(int tabIndex) {
    emit(state.copyWith(tabIndex: tabIndex));
  }

  void changeTabByTitle(String title) {
    final tabIndex = HomePage.views.keys.toList().indexOf(title);
    changeTab(tabIndex);
  }

  Future<bool> onBackButtonPressed() async {
    if (state.tabIndex == defaultTabIndex) {
      return true;
    } else {
      changeTab(defaultTabIndex);
      return false;
    }
  }
}

Transformers BLoC

  1. Concurrent — Process incoming event of bloc concurrently
  2. Sequential — Process incoming events one after another
  3. Droppable — Drops the incoming events if one event is being processed
  4. Restartable — Process the latest incoming event and stop the execution of the ongoing event

BLoC Vs Cubit

BLoC

  • Bloc Takes Events and Gives State
  • Bloc has Events and States as Streams
  • Bloc has a limitation and that is without adding the Event we cannot emit any states. Events are added first and only the State is emitted.

CUBIT

  • Cubit is the subset of BLoC package.
  • Cubit is lightweight compared to bloc.
  • Cubit does not have Event Streams and only has State Streams
  • Events are replaced by functions so simply we need to call the function to get the States. So this reduces the extra boilerplate codes.
    While in Cubit in a single function call, we can emit multiple times the States.

There is only a difference between the creation of State and emitting between Bloc and Cubit but there is no difference between using it.

BLoC Vs GetX

  • BLoc: It is based on the principle of unidirectional data flow, which means that the state of the app is always updated in a predictable way. This makes it easier to debug and maintain large and complex apps.
  • GetX: It uses a reactive programming approach, which can make it easier to develop dynamic and responsive user interfaces.
  • BLoC: Bloc is a powerful framework that offers a lot of features such as Stream management, Event handling, and State management.
  • GetX: It is a package that offers a range of features such as Dependency Injection (DI), Routing, State Management, and many more. GetX is a good choice for small, medium-sized and large applications because of its simplicity and ease of use.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top