Flutter Bloc: A Complete Guide

Flutter Bloc A Complete Guide

In this guide, we will learn a complete guide Bloc as state management in a flutter. When building an application, Flutter is the easiest and most powerful framework. But building an application without any strong architecture is like building a house without any planning and blueprints. We won’t understand the uses of architecture when building small applications. when it comes to building a big production level application where you have many screens, animations, methods, classes, etc, without any proper architecture you will end up in a state where everything is messed up and you don’t know how all the components, classes, methods are communicating and functioning. So it is very necessary to maintain the code to make code more readable and testable, and easily trackable when designing and developing this kind of big application. There are many different packages available, and all have their own way to handle application states.

In this article, we are going to talk about Bloc and a complete Guide on it. Bloc is not just state management, but it’s also an architectural design pattern that helps us to build production-level applications.

What is Bloc?

  1. Bloc stands for Business Logic Component.
  2. It helps to separate business logic from the presentation layer and enables a developer to reuse code more efficiently
  3. created and maintained by Felix Angelo
  4. It helps in managing state and make access to data from a central place in our project.
  5.  relies on events to trigger state changes rather than functions.
  6. receive events and convert the incoming events into outgoing states.
  7.  the Bloc class extends BlocBase, we have access to the current state of the bloc at any point in time via the state getter just like in Cubit.
  8. Blocs should never directly emit new states. Instead, every state change must be output in response to an incoming event within an EventHandler.
  9. One key differentiating factor between Bloc and Cubit is that because Bloc is event-driven, we are also able to capture information about what triggered the state change.
  10. they allow us to override onEvent which is called whenever a new event is added to the Bloc.
  11. Bloc has an event sink that allows us to control and transform the incoming flow of events.

The above diagram shows how the data flow from UI to the Data layer and vice versa. BLOC will never have any reference for the Widgets on the UI Screen. The UI screen will only observe changes coming from the BLOC class.

The Bloc is distinguished into four layers.

1. UI (Presentation Layer):

All the component(Widgets) of the app which is visible to the user is defined here.

  • The presentation layer’s responsibility is to figure out how to render itself based on one or more bloc states.
  • In addition, it should handle user input and application lifecycle events. The presentation layer will have to figure out what to render on the screen based on the state from the bloc layer.

2. Bloc (Business Logic Layer):

It acts as a middle man between UI and Data layer.

  • Bloc takes an event triggered by the user (ex: GetWeatherData button press, Submit form button press, etc) as an input, and responds back to the UI with the relevant state.
  • The business logic layer’s responsibility is to respond to input from the presentation layer with new states.
  • This layer can depend on one or more repositories to retrieve data needed to build up the application state.
  • The business logic layer is notified of events/actions from the presentation layer and then communicates with the repository in order to build a new state for the presentation layer to consume.

3. Data Layer:

This layer has further two parts. Repository and Data Provider.

datalayer.png

3.1 Data Provider:

This layer retrieves/fetches the raw data from different data sources (ex: different APIs, DBs, networks, Shared preferences, etc).

  • For example: If you are building a weather app. Then you might use external APIs like OpenWeatherAPI, from where you will get raw data.
  • You can have GETPOSTDELETE, etc methods inside this class. 
  • The data provider’s responsibility is to provide raw data. The data provider should be generic and versatile.
  • The data provider will usually expose simple APIs to perform  CRUD operations.
  • We might have a createDatareadDataupdateData, and deleteData the method as part of our data layer.
import 'package:http/http.dart' as http;

class DataService {
  Future<http.Response?> getPosts() async {
    const _baseUrl = 'jsonplaceholder.typicode.com';
    try {
      final url = Uri.https(_baseUrl, '/posts');
      final response = await http.get(url);
      return response;
    } catch (e) {
      rethrow;
    }
  }
}

3.2 Repository:

This layer contains one or more than one Data providers.

  • The transformation is done on the raw data returned by the Data Provider in this layer. (Forex: converting them raw into some kind of Model). 
  • Bloc communicates with this layer when the user requests the data.
  • This layer requests raw data from the Data Provider and after that, this layer performs some kind of transformation
  • The repository layer is a wrapper around one or more data providers with which the Bloc Layer communicates.
// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';

import 'package:bloc_api/Model/post.dart';
import 'package:bloc_api/Service/data_service.dart';

class ApiRepository {
  final DataService dataService;
  ApiRepository({
    required this.dataService,
  });

  Future<List<Post>> getPostsList() async {
    final response = await dataService.getPosts();
    if (response!.statusCode == 200) {
      final json = jsonDecode(response.body) as List;
      final posts = json.map((e) => Post.fromJson(e)).toList();
      return posts;
    } else {
      throw Exception('Failed to load Posts');
    }
  }
}

Two important terms that we need to understand: Event and State.

Event: Event is nothing but different actions (button click, submit, etc) triggered by the user from UI. It contains information about the action and gives it to the Bloc to handle. 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. 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.

State: The UI will update according to  State what was received from the Bloc. For example, there could be different kinds of states:

LoadingState - Will Show Progress Indicator
LoadedState - Will Show Actual widget with data
ErrorState - Will show an error that something went wrong.

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, in a real example it will be like that:

1- The user clicks on a button to get a list of games.

2- The event is triggered and it informed to the bloc that the user wants a list of games.

3- The bloc is going to request this data ( from a repository for example, which is in charge of connecting to the API to get the data).

4- When the bloc has the data it will determine if the data is a Success or is Error, and then it’s going to emit a state.

5- The view is going to be listening to all the possible states that the bloc could emit to react to them. For instance, if bloc emits Success as a state the view is going to rebuild it with a list of games, but if the state is Error the view is going to rebuild with an error message or whatever you want to show.

Implementing Bloc

Before we dive into implementation VS Code has a very handy extension for Bloc which is named a bloc. Make sure we have installed it.

1: Create a Flutter Application

flutter create bloc_practise

2: Go to pubspec.yaml and add flutter_bloc packages inside dependencies

dependencies:
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.1
 

3: Create bloc files: Now right-click on the lib folder of our application, here we’ll see Bloc: New Bloc option if we’ve installed the bloc extension

bloc_create.png

After clicking on this, the dialog will pop up for giving the name to the bloc. Give the name and press Enter.

Now If we see in the lib directory the folder named bloc is created and it has 3 different files – counter_bloccounter_event, and counter_state

Counter_bloc.dart

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';

part 'counter_event.dart';
part 'counter_state.dart';

class CountersBloc extends Bloc<CountersEvent, CountersState> {
  CountersBloc() : super(CountersInitial()) {
    on<CountersEvent>((event, emit) {
      // TODO: implement event handler
    });
  }
}

The counter_bloc.dart class is a bridge between our UI and the Data class, In other words, this class will handle all the Events triggered by the User and sends the relevant State back to the UI.

bloc.png
  • We are extending our counterbloc class  Bloc which takes two things counterEvent and counterState. As the name suggests, they handle  State and Events respectively.
  • In the second line, the constructor of our class is created. In this, we need to provide a initial state. It’s not going to do anything. It simply represents that the app is now in its initial stage and nothing has happened yet.

counter_event.dart

part of 'counter_bloc.dart';

@immutable
abstract class CounterEvent {}

class CounterIncrement extends CounterEvent {}

class Counterdecrement extends CounterEvent {}
  • In this class, we define different kinds of events by extending the abstract event class.
  • For example, when the user presses a button, there is 2 button that is increment and decrement button.
  • Now let’s implement weather_state.dart class

counter_state.dart

// ignore_for_file: public_member_api_docs, sort_constructors_first

part of 'counter_bloc.dart';

@immutable
class CounterBlocState  {
  final int counter;
  const CounterBlocState({
    required this.counter,
  });

}
class CounterInitial extends CounterBlocState {
  const CounterInitial() : super(counter: 0);
}
class IncrementState extends CounterBlocState {
 const IncrementState(int increment) : super(counter: increment)
}
class DecrementState extends CounterBlocState {
const DecrementState(int increment) : super(counter: increment);
}

In this class, we define different kinds of states (For example, initial state, increment state, decrement state).

Back to counter_bloc.dart:

import 'package:equatable/equatable.dart';

import 'package:meta/meta.dart';
part 'counter_event.dart';
part 'counter_state.dart';

class CounterBloc extends Bloc<CounterEvent, CounterBlocState> {
  CounterBloc() : super(const CounterInitial()) {
    on<CounterIncrement>((event, emit) {
      emit(IncrementState(state.counter + 1));
    });
    on<Counterdecrement>((event, emit) {
      emit(DecrementState(state.counter - 1));
    });
  }

}

4: How to access bloc data in UI?

  • we have created the bloc and implemented all the functionalities we need to somehow provide this bloc to the widget tree so that we can use the data and display it on the screen.
  • Before that, we have to understand different Widgets provided by bloc:

  1. BlocProvider
  2. BlocBuilder
  3. BlocListener
  4. BlocConsumer
  5. RepositoryProvider

1. BlocProvider

  • provides a bloc to its children (i.e Widgets).
  • used as a dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.
  • We have to put it at the place from where all the children can access the bloc.
  • So let’s wrap the BlocProvider inside MaterialApp
 Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Bloc',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BlocProvider(
        create: (context) =>
            CounterBloc()
),
        child: const HomeScreen(),
      ),
    );
  }
  • Now we can access our bloc via BlocProvider.of<counterBloc>
  • BlocProvider has one lazy parameter. By default, it’s true. It is used to lazily load the bloc. It means whenever anyone tries to use them bloc then it will be initialized.
  • To override this functionality we can change the value of lazy to false.
  • Now we might have asked a question, what if we have multiple blocs. How can we provide all the bloc from the main.dart? we will use a MultiBlocProvider for it.

MultiBlocProvider:

It provides us a MultiBlocProvider widget that takes a List of Bloc and provides it to its children. Let me demonstrate

 MultiBlocProvider(
              providers: [
                BlocProvider(
                  create: (context) => CounterCubit(),
                ),
                BlocProvider(
                  create: (context) => CounterBloc(),
                ),
                BlocProvider(
                  create: (context) => ThemeCubit(),
                ),
              ],
              child: MyApp(
                appRouter: AppRouter(),
              ),
            ),
          ),

2. BlocBuilder:

  • BlocBuilder is a widget that helps Re-building the UI based on State changes.
  • In our case we want our UI to update the state when the user presses the incremnet and decrement button.
  • BlocBuilder builds the UI every single time state changes
  • So, it’s very necessary to place BlocBuilder around the Widget that we want to rebuild.
  • we can also wrap the whole Widget inside the BlocBuilder (i.e around the Scaffold), but it’s not a good way. Because think about the time and processing power that will be consumed when your whole widget tree rebuilds just to update a Text widget inside the tree. So make sure you wrap the BlocBuilder around the widget that needs to be rebuilt when the state changes.
body: BlocBuilder<CounterBloc, CounterState>(
      builder: (context, state) {
        return ...
   }
)
  • BlocBuilder takes two things bloc, and state.
  • builder the function is required which takes two parameters. context and the state which is of type WeatherState in our case. And it should return Widget in response.
  • We can explicitly provide the bloc in BlocBuilder bypassing the bloc inside bloc the property of BlocBuilder
BlocBuilder<CounterBloc, CounterState>(
bloc: blocA, // provide the local bloc instance
builder: (context, state) {
   return ...
}
)
  • If the bloc parameter is omitted, BlocBuilder will automatically perform a lookup using BlocProvider and the current BuildContext.
  • Only specify the bloc if you wish to provide a bloc that will be scoped to a single widget and isn’t accessible via a parent BlocProvider and the current BuildContext.
  • buildWhen the parameter takes the previous bloc state and current bloc state and returns a boolean. If buildWhen returns true, builder will be called with state and the widget will rebuild. If buildWhen returns are false, builder will not be called with state and no rebuild will occur.
BlocBuilder<CounterBloc, CounterState>(
buildWhen: (previousState, state) {
},
builder: (context, state) {
  return ...
}
)
  • we can access your Bloc by – BlocProvider.of(context) or context.
context.read<WeatherBloc>().add(Decrement());
// or
BlocProvider.of<WeatherBloc>(context)add(Decrement())

3. BlocListener:

  • As the name suggests, this will listen to any state change as BlocBuilder does.
  • But instead of building the widget like BlocBuilder, it takes one function, listenerwhich is called only once per state, not including the initial state.
  • Example: Navigation, Showing a SnackBar, Showing a Dialog, etc…
  • It also has a bloc parameter. Only specify  bloc if you wish to provide a bloc that is otherwise not accessible via BlocProvider and the current BuildContext.
  • The listenWhen the parameter is the same as BlocBuilder‘s buildWhen but for Listener.
  • The whole idea of BlocListener is – It is not responsible for building/updating the widget as BlocBuilder does.
  • It only listens to the state changes and performs some operations. The operation could be (Navigating to other screens when state changes, Showing Snackbar on a particular state, etc).
  • Let’s say we want to show a snackbar on IncrementState state –
  • Wrap the body inside BlocListener.
BlocListener<CounterBloc, CounterState>(
              listener: (context, state) {
                if (state is IncrementState) {
                  ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                     content: Text('Successfully Increment'),
                    duration: const Duration(milliseconds: 300),
                  ));
                } else {
                  if (state is DecrementState) {
                    ScaffoldMessenger.of(context).showSnackBar(SnackBar(
                      content: Text('Successfully Decrement'),
                      duration: const Duration(milliseconds: 300),
                    ));
                  }
                }
              },
              child:  builder: (context, state) {
              return Text(
                state.counter.toString(),
                style:
                    const TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
              );
            },
                },
              ),
            ),
          ),

4. MultiBlocListener:

It is a Flutter widget that merges multiple BlocListener widgets into one. MultiBlocListener improves the readability and eliminates the need to nest multiple BlocListeners

BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
child: BlocListener<BlocB, BlocBState>(
  listener: (context, state) {},
  child: BlocListener<BlocC, BlocCState>(
    listener: (context, state) {},
    child: ChildA(),
  ),
),
)

TO

MultiBlocListener(
listeners: [
  BlocListener<BlocA, BlocAState>(
    listener: (context, state) {},
  ),
  BlocListener<BlocB, BlocBState>(
    listener: (context, state) {},
  ),
  BlocListener<BlocC, BlocCState>(
    listener: (context, state) {},
  ),
],
child: ChildA(),
)

5. BlocConsumer:

  • As of now we are building a widget using BlocBuilder and showing the snackbar using BlocListener. Is there any easy way to combine both in a single widget?
  • Bloc provides a BlocConsumer widget, which combines both BlocListener and BlocBuilder.
  • So instead of writing BlocListener and BlocBuilder separately, we can do –
 BlocConsumer<CounterBloc, CounterBlocState>(
            listener: (context, state) {
              if (state is IncrementState) {
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(
                    content: Text('Successfully Increment'),
                    duration: Duration(milliseconds: 300),
                  ),
                );
              } else {
                if (state is DecrementState) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(
                      content: Text('Successfully Decrement'),
                      duration: Duration(milliseconds: 300),
                    ),
                  );
                }
              }
            },
            builder: (context, state) {
              return Text(
                state.counter.toString(),
                style:
                    const TextStyle(fontSize: 50, fontWeight: FontWeight.bold),
              );
            },
          ),
RepositoryProvider:
  • It is the same widget as BlocProvider.
  • But the main difference is that BlocProvider provides a single instance of bloc to its children whereas RepositoryProvider provides repositories to its children.
  • It is used as dependency injection (DI) widget so that a single instance of a repository can be provided to multiple widgets within a subtree.
RepositoryProvider(
create: (context) => CounterRepository(),
child: ChildWidget(),
);

The widget can access this repository by –

context.read<CounterRepository>();
// or
RepositoryProvider.of<CounterRepository>(context)
MultiRepositoryProvider:

As the name suggests, it provides multiple repositories. example:

MultiRepositoryProvider(
providers: [
  RepositoryProvider<RepositoryA>(
    create: (context) => RepositoryA(),
  ),
  RepositoryProvider<RepositoryB>(
    create: (context) => RepositoryB(),
  ),
  RepositoryProvider<RepositoryC>(
    create: (context) => RepositoryC(),
  ),
],
child: ChildA(),
)
BlocSelector: 
  • It is a Flutter widget that is analogous to BlocBuilder but allows developers to filter updates by selecting a new value based on the current bloc state.
  • The selected value must be immutable in order for BlocSelector to accurately determine whether builder should be called again.
  • If the bloc the 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.
  },
)

For the Code Snippet Download from here

Also Read:

Riverpod As An StateManagement

Flutter State Management: GetX As An StateManagement