Build Better Flutter Applications, Part 2

A Deeper Look at BLoC Pattern for Flutter

By Murat Cezan

In part 1 of this two-part series on the Bloc Pattern for Flutter, I defined the Business Logic Components (BloC) pattern as a reactive pattern that separates an application's business logic from its UI logic. The purpose is to make the code easier to maintain and test. I also provided some insight into reactive programming and how it differs from traditional, event-driven programming.

In this installment I’ll dive deeper into the BloC Pattern, exploring various aspects, such as BloC Provider and BloC Builder, and share with you some code examples to help you better understand this important design pattern.

First, some necessary packages must be installed in order to use BLoC. These packages will provide us with Widgets to be used with BLoC.

Let's go to the project directory and add the flutter_bloc package:

murat@ICS:~/bloc_test$ flutter pub get flutter_bloc

BlocProvider

BlocProvider is a widget used to define the BLoC in the widget tree. In this way, we can access the data or functions we work in BLoC on our page. BlocProvider also automatically turns off the BLoC it creates. Thus, no data is leaked from the Stream.

BlocProvider does not create a new instance for each sub-branch widget. It uses the same Bloc instance in all sub-branches starting from the provided branch. While creating the Bloc instance, the context it wants as a parameter is used to understand the location of the Provider in the Widget tree. Thus, the BlocProvider can access information such as how many widgets are in its sub-branches.

Note that you cannot use the same Bloc with a new page because the context of the other page will be different. Instead, you must use the BlocProvider.value Widget to use it. BlocProvider.value cannot automatically turn off BLoC because it may still be desired to use Bloc Instance on another page.

Here’s how to use BlocProvider:

Import the BlocProvider widget.

import 'package:flutter_bloc/flutter_bloc.dart';

Create the BlocProvider widget inside your application's root widget and provide your BLoC:

BlocProvider(
 create: (BuildContext context) => MyBloc(),
 child: MyApp(),
)

Use the BlocBuilder or BlocListener widgets to access your BLoC.

BlocBuilder

This is a widget that allows developers to rebuild the UI according to changes in Bloc or Cubit's state. Where it will be used is important. If the BlocProvider is defined from the top, as in the graphic below, it will rebuild the entire tree when the State of the bloc is changed and will cause performance loss. In short, it will be no different from setState.

Incorrect use of BLoCProvider

The correct method is to rebuild on the UI Component that it will refresh, as shown in the diagram below:

Correct use of BLoCProvider

BlocBuilder can be called more than once due to its working principle in the Flutter engine. Therefore, some widgets that want to run should be called with BlocListener, not BlocBuilder. See the example below:

BlocBuilder<MyBloc, MyState>(
 builder: (context, state) {
   if (state is LoadingState) {
     return Center(
       child: CircularProgressIndicator(),
     );
   } else if (state is LoadedState) {
     return Text(state.data);
   } else {
     return Text('Error');
   }
 },
)

BlocListener

This is a Flutter widget that has the same structure as BlocBuilder but has different uses. Since BlocBuilder is a callable widget by nature, it would be inappropriate to use page transitions, snacBar display, or showDialog display if they occur when BlocBuilder is called more than once.

That’s where BlocListener comes in handy. Here’s how it’s used:

  • It listens to State changes. Unlike BlocBuilder, the widget cannot be refreshed. The listener requesting a void function takes a parameter.
  • In the Listener function, what will be done in State changes, including Initial State, is determined.
BlocListener<MyBloc, MyState>(
 listener: (context, state) {
   if (state is ErrorState) {
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Error'),
       ),
     );
   }
 },
 child: BlocBuilder<MyBloc, MyState>(
   builder: (context, state) {
     if (state is LoadingState) {
       return Center(
         child: CircularProgressIndicator(),
       );
     } else if (state is LoadedState) {
       return Text(state.data);
     } else {
       return Text('Default Text');
     }
   },
 ),
);

BLocConsumer

BlocConsumer is a widget for using BLoC architecture in Flutter applications. This widget performs two separate actions depending on the status of the BLoC:

  1. When the BLoC state changes, it executes a transaction and updates the interface.
  2. If the user triggers the action, it executes an action and updates the interface.

BlocConsumer takes the following parameters:

BLoC type: Specifies the BLoC type the BlocConsumer will listen to.

Status type: Specifies the type of statuses broadcast from the BLoC.

Builder function: Used to build a widget tree based on the state of the BLoC.

Transaction handler: The transaction handler is executed when the user triggers a transaction.

In the following code example, I create a text widget and a button widget based on the state of a BLoC. When the button is pressed, a transaction handler is assigned to the BLoC to execute a transaction:

BlocConsumer<MyBloc, MyState>(
 builder: (context, state) {
   return Text(state.data);
 },
 listener: (context, state) {
   if (state is ErrorState) {
     ScaffoldMessenger.of(context).showSnackBar(
       SnackBar(
         content: Text('Error'),
       ),
     );
   }
 },

RepositoryProvider

RepositoryProvider is a bloc package widget used in Flutter applications to provide easy access to the classes in which data storage operations are executed. This widget is typically used to provide access to API calls, database operations, and other external data sources.

RepositoryProvider allows a single instance of any class to be created and shared across the application. Therefore, widgets in different parts of the application can receive and use the same instance through the RepositoryProvider. For example, suppose a UserRepository class is created and that class executes API calls. This class can be created using RepositoryProvider and added to the widget tree, making it accessible to widgets anywhere in the application.

void main() {
 runApp(
   RepositoryProvider(
     create: (BuildContext context) => UserRepository(),
     child: MaterialApp(
       home: MyHomePage(),
     ),
   ),
 );
}

MultiBlocProvider

MultiBlocProvider is a widget for using multiple BLoCs in Flutter apps. This widget is used to provide multiple BLoCs and pass those BLoCs to all subtrees under it. The MultiBlocProvider takes the following parameters:

Providers: A list that specifies all BLoC providers that will be provisioned to the MultiBlocProvider.

Child: Widget tree to migrate BLoCs.

In this example, I use MultiBlocProvider to pass two separate BLoC providers:

MultiBlocProvider(
 providers: [
   BlocProvider<MyBloc1>(
     create: (context) => MyBloc1(),
   ),
   BlocProvider<MyBloc2>(
     create: (context) => MyBloc2(),
   ),
 ],
 child: MyApp(),
);

MultiBlocListener

MultiBlocListener is a Flutter widget used to listen for multiple BLoCs. It passes those BLoCs to all subtrees under it. MultiBlocListener takes the following parameters:

Listeners: A list specifying all BLoC listeners that will be supplied to the MultiBlocListener.

Child: Widget tree to listen for BLoCs.

In this example I use it to pass two separate BLoC listeners using MultiBlocListener:

MultiBlocListener(
 listeners: [
   BlocListener<MyBloc1, MyState1>(
     listener: (context, state) {
       // MyBloc1 listener operations
     },
   ),
   BlocListener<MyBloc2, MyState2>(
     listener: (context, state) {
       // MyBloc2 listener operations
     },
   ),
 ],
 child: MyApp(),
);

MultiRepositoryProvider

MultiRepositoryProvider is a Flutter bloc package widget for accessing multiple repositories. This widget combines multiple RepositoryProviders, allowing widgets in different parts of the application to access different data sources.

For example, an application might need both a UserRepository and a ProductRepository. In this case, you can create both repositories and share them application-wide using a MultiRepositoryProvider. This way, widgets anywhere in the application can access the repositories they need through the MultiRepositoryProvider.

void main() {
 runApp(
   MultiRepositoryProvider(
     providers: [
       RepositoryProvider<UserRepository>(
         create: (BuildContext context) => UserRepository(),
       ),
       RepositoryProvider<ProductRepository>(
         create: (BuildContext context) => ProductRepository(),
       ),
     ],
     child: MaterialApp(
       home: MyHomePage(),
     ),
   ),
 );
}

Summary

To recap, BLoC is built on a reactive programming architecture. Thanks to the advantages of reactive programming, the performance of applications developed with Flutter is better than that of applications built using other methods. Using reactive programming also extends the lifecycle of the code and provides an open-ended architecture.

In the next installment in the series, I’ll explain how to conduct unit tests using BLoC. And in case you missed part 1 in this series, I shared the widgets provided by BLoC with sample code. Fully understanding these widgets and adding explanation to the developed code will allow you to have a cleaner architecture.