Cyberithub

Understanding the Concepts of BLoC Pattern in Flutter with Examples

Advertisements

In this article, we will go through the concepts of BLoC pattern in flutter with examples. Managing the states in apps is, besides managing UIs, the hardest part. The process of creating a flutter app has involved multiple state management, and it has been difficult to compete in the market. To address the various issues and difficulties that developers face when creating apps, various state management systems have been developed.

Although there are still many state management options that are superior to the BLoC structure, BLoC is not the newest one on the market. However, the majority of flutter developers use it as one of the most effective state management techniques, and the flutter team also views it as their top choice.

 

What is BLoC

A tool or package for state management is BLoC. "Business Logic Component" is what it stands for. Separating business logic from the presentation layer is the main goal of the BLoC pattern.

The core values of BLoC, according to its creators, are straightforward, potent, and verifiable. The BLoC pattern is made to be so easy to understand that everything is broken down into layer concepts, just like we do with websites (frontend and backend), and breaks apps into layers (data layer, domain layer, and presentation layer).

 

Understanding the Concepts of BLoC Pattern in Flutter with Examples

Understanding the Concepts of BLoC Pattern in Flutter with Examples

Also Read: What are Reusable Components in Flutter [Explained with example]

Well, BLoC is primarily designed to distinguish between the various app layers and force each layer to adhere to a particular pattern in order to be understood and managed. Thus, BLoC divides an application into three layers:-

1. Data Layer: The data layer is the lowest level of an application and is primarily responsible for interacting with and retrieving data from various sources, including APIs, databases, etc. Essentially, the data layer is divided into two sections:-

  • Repository: The repository is a component of the data layer that acts as a wrapper for one or more data providers and primarily manages and supports BLoC layer communication.
  • Data Provider: This component of the data layer exposes APIs so that users can create, update, delete, and read data.

2. Business Logic layer: Also known as BLoC, is named primarily for this central logic, or business logic component. This layer primarily serves as a link between the data layer and the presentation layer. It primarily receives events, actions, and notifications from the presentation layer before using repositories to connect to the data layer and initiate new states in response to those specific actions, events, or notifications.

3. Presentation Layer: In terms of the BLoC pattern, the presentation layer is the top layer. It primarily consists of what a user sees in an app. The presentation layer must handle user input and application lifecycle events in addition to rendering itself based on one or more states of the block.

 

How does Application Layers Interact 

You might have wondered as a developer how the application layer interact with one another when the state changes since they operate as separate layers and typically have little to no interlinking. The simple solution is for each of them to subscribe to the streams that Bloc exposes, listen to the changes, and then make the necessary changes.

To avoid high coupling and difficult maintenance, siblings dependencies between two entities in the same architecture layer should be avoided at all costs. Additionally, since a bloc is a component of the business logic layer, none of the other blocs ought to be aware of it.

A bloc typically has two choices when it wants to respond to another bloc: either calling the presentation layer, which is the layer above it, or calling the domain layer, which is the layer below it.

 

Using Presentation Layer to connect Blocs

The code below uses a BlocListener to listen to a specific block and add an event to a different block when the state of the first block changes.

class HomePage extends StatelessWidget{
 const HomePage({Key? key}) : super(key: key);

@override
Widget build(BuildContext context){
 return BlocListener<HomeCubit, HomeState>(
  listener: (context, state){
   BlocProvider.of<RegisterBloc>(context).add(LoadRegisterEvent());
  },
  child: TextButton(
    child: const Text('Login'),
    onPressed:(){
     BlocProvider.of<LoginBloc>(context).add(LoadLoginEvent());
    },
   ),
  );
 }
}

 

Using Domain Layer to connect Blocs

Sometimes you may not want to connect two blocs in the presentation layer. It makes more sense when you connect two blocs to the same source of data such as a stream from a repository. So that whenever repository data changes, blocs can update their states independently of one another by listening to streams from the repository.

Similar to this, we can inject that repository, which must respond in response to the state change, after having it expose a stream. First we need to create or use a repository which provides a data Stream. For example, below repository exposes a never ending stream.

class LoginRepository{
 int _currentUserId= 0;
 final List<String> _userIds= ['2546847','1684864','1848484'];
 Stream<String> productUserIds() async* {
  while(true){
   yield _userIds[_currentUserId ++ % _userIds.length];
   await Future<void>.delayed(const Duration(minutes: 1));
  }
 }
}

Now the same repository can be injected into each bloc that needs to react to new User Ids. Below is the UserIdBloc which yields a state out for each incoming User Id from the repository above.

class UserIdBloc
  extends Bloc<UserIdScoreboardEvent, UserIdScoreboardState>{
UserIdBloc({required UserIdRepository userIdRepo})
  :_userIdRepo=userIdRepo,
   super(UserIdInitialScoreboardState()){
 on<UserIdStartScoreboardState>((event, emit)async{
  await emit.forEach(
   _userIdRepo.productUserIds(),
   onData:(String idea) => UserIdScoreboardState(idea: idea),
  );
 });
}

final UserIdRepository _userIdRepo;
}

 

Why should we use BLoC

Well, interestingly, BLoC isn't just state management for Flutter apps; it is also state management for Angular, called angular_bloc. BLoC also has its own test library, an automatic persisting and bloc state restoring library, and a library for undoing and redoing state management using BLoC. It has built a large base around it so that people can understand and take a deep dive into the BLoC pattern.

 

Bonus: Testing in BloC

Well, Bloc is built in such a way that it can help the developers from all perspectives, including the tools, mindset, and other problem-solving abilities. BLoC also comes with its own testing concepts, which makes it easier for the developer to debug and solve the issue in the environment of BLoC. For applying the logic of Bloc testing, we have to install its package called ‘bloc_test’ in our pubspec.yaml.

dev_dependencies:
 test: ^1.16.0
 bloc_test: ^9.1.0

This code mainly triggers the event functions, when a particular event is triggered. For example - when the CounterIncrementPressed event is triggered, the value of the state is increased by 1. Similarly, for the decrement case.

abstract class CounterEvent{}
class CounterIncrementPressed extends CounterEvent{}
class CounterDecrementPressed extends CounterEvent{}

class CounterBloc extends Bloc<CounterEvent, int>{
 CounterBloc():super(0){
  on<CounterIncrementPressed>((event, emit) => emit(state + 1));
  on<CounterDecrementPressed>((event, emit) => emit(state - 1));
 }
}

The code mentioned below is primarily what we write in a typical testing scenario.

void main() {
 group('Testing CounterBloc',(){
  late CounterBloc counterBloc;
  setUp(() {
   counterBloc = CounterBloc();  // initializing bloc
  });
  test('initial state is 0',(){
   expect(counterBloc.state, 0);
  });
 });
}

Now, let's look at how Bloc differs from it and streamlines the process from the standpoint of testing apps in Flutter. Here is the code to test CounterIncrementPressed event.

blocTest(
'emits [1] when CounterIncrementPressed is added',
build: () => counterBloc,
act: (bloc) => bloc.add(CounterIncrementPressed()),
expect: () => [1],
);

Above test includes the followings:-

  • build: It is used to create BLoC
  • act: It is used to add event or to call method to trigger BLoc
  • expect: It is a simple iterable used to ensure that act called from previous parameter will return states as given here.

Here is the code to test CounterDecrementPressed event.

blocTest(
 'emits [-1] when CounterDecrementPressed is added',
 build: () => counterBloc,
 act: (bloc) => bloc.add(CounterDecrementPressed()),
 expect: () => [-1],
);

As we can see from the code above, we are initializing a block within the test itself, and we have additional parameters that ask us to provide a response when a specific action or event is triggered.

 

Conclusion

In this article, we learned about bloc pattern in Flutter and how bloc as a state management tool, helps us accomplish the majority of the tasks. Along with that we have also seen how it is compatible with Flutter and simple to understand. We also learned how bloc helps in interacting with various application layers when the state changes. Finally, we discussed about few testing concepts in bloc.

Leave a Comment