Explaining Flutter BloC to 5yrs old Me

A beginner's guide to understanding bloc state management

Table of contents

No heading

No headings in the article.

Mobile Applications have become a part of our everyday lives, hence it has become necessary to arm your flutter application with all the functionalities the user might find useful and in most cases, the user might want to be on multiple things at the same time, so the problem here is how does your flutter application keep track of where and what the user is currently on? that's one of the problems state management is supposed to help us solve. "Best State management" is currently the most controversial discussion in the flutter community, but I won't want to dabble into that since I believe every state management is the best💙.

Why Bloc: Bloc stands for Business Logic Components, it is an architectural pattern just like MVVC and the rest, its primary goal is to separate your UI from your business logic and make your codebase simpler, neater and more readable. it is also a very popular state management solution in the flutter ecosystem

Flutter Bloc: This package provides you with widgets that help you integrate bloc seamlessly.

Bloc Terms:

Events - These are actions taken or to be taken either by the user or by the flutter app on behalf of the user. You create events by having an abstract class called the name of your events and extend it to your abstracted event class whenever you want to create a new event.


abstract class TestEvent extends Equatable {
  const TestEvent();
}

class TouchEvent extends TestEvent {
  final int pageNumber;
  final int contentSize;
  final Map<String, dynamic> whereCondition;

  TouchEvent({
    this.condition = const {},
    this.pageNumber = 1,
    this.contentSize = 5,
  });
  @override
  List<Object> get props => [pageNumber, contentSize, condition];
}

In our example above we only have one event, which is a touch event and we're expected to provide "pageNumber", "pageSize and "condition" and we have default values specified. Equatable is used to compare classes, it is very useful when testing.

State - Like the name suggest these are the possible state the application can be in and it is worthy of note that you can only be in one state at a time unless there's a future solution to that.

abstract class TestState extends Equatable {
  const TestState();
}

class TestBlocInitialState extends TestState {
  @override
  List<Object> get props => [];
}

class TestBlocLoading extends TestState {
  @override
  List<Object> get props => [];
}

class TestBlocSuccessState extends TestState {
  final TestApiResponse testApiResponse;

  TestBlocSuccessState({required this.testApiResponse});
  @override
  List<Object> get props => [testApiResponse];
}

class TestBlocFailureState extends TestState {
  final String? error;

  TestBlocFailureState({this.error});
  @override
  List<Object?> get props => [error];
}

in the code above you can see we have TestState, and the several possible states we can have with the bloc, we have:

  1. TestBlocInitialState - this will serve as our initial page
  2. TestBlocLoading - this will be emitted when we're waiting for a response.
  3. TestBlocSuccessState - this will be emitted if the request was successful.
  4. TestBlocFailureState - this will be emitted if the request fails

Bloc - this is where the magic happens, you emit states based on the event(action) from the UI level.

class TestBloc extends Bloc<TestEvent, TestState> {
  TestBloc() : super(TestBlocInitialState()) {
    on<TouchEvent>(_getTestFunction);
  }

  TestRepository _testRepository = TestRepository.instance;
  void _getTestFunction(TouchEvent event, Emitter<TestState> emit) async {
    emit(TestBlocLoading());

    Tuple2<TestApiResponse?, String?> response =
        await _testRepository.testMethod();

    if (response.item1 is TestApiResponse) {
      emit(TestBlocSuccessState(testApiResponse: response.item1!));
    } else if (response.item2 is String) {
      emit(TestBlocFailureState(error: response.item2));
    }
  }
}

First, you have to give your bloc a name which is a class that extends Bloc of type of your state and event, after which you initialise your bloc, and provide a state which is supposed the be the first state you want the user on when they come to that part of your app. Bloc works as a stream so once you've initialised it, it stays alive to await events(instructions), and whenever "TouchEvent" is triggered, the corresponding function is called and in our case, it is "getTestFunction", so this function takes in the event and emitter of type of our state.

1. The event carries all the parameters we pass to its
2. Emitter helps us emit the respective states back to the UI based on the outcome of our logic

inside our "getTestFunction" we make a network call and based on the response, we return either success or failed state to the UI.

PS:

`It is ideal to have three files in each bloc folder and the file names will be

1. functionality_bloc.dart
2. functionality_state.dart
3. functionality_event.dart
where *functionality is the name of the functionality you're creating the bloc for.

UI: Firstly you need to make your bloc available above your widget tree for your children widget to have access to it, you could take it a step higher or however, you prefer.

class TestScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider<TestBloc>(
      create: (context) => TestBloc()
        ..add(
          TouchEvent(),
        ),
      child: TestUi(),
    );
  }
}

so what we did was initialise our TestBloc and actioned the touch event, so our expectation is whenever the user comes into the page we'll make an API call to fetch some data.

Cosumers:

There are 3 ways you can consume your bloc.

  1. BlocListener: This listens for changes of state in your bloc and emits the state and it is important to note that BlocListener does not rebuild your widget, hence it should for used for function-based operations.
BlocListener<TestBloc, TestState>(
              listenWhen: (previousState, nextState){
                if(previousState is TestBlocLoading){
                  stopLoader();
                }
                return true;
              },
              listener: (context, state) {
                if (state is TestBlocLoading) {
                  showLoader();
                }
                if (state is TestBlocSuccessState) {
                  Navigator.pushNamed(context, "next_route");
                }
                if (state is TestBlocFailureState) {
                   showToast(error: state.error);
                }
              },
              child: SizedBox(),
            ),

it is important you specify the state and event on the bloc and also note that you can track the prev and next state of your bloc with listenWhen, which gives you two parameters, the previous state and the next state.

listenWhen is optional

  1. BlocBuilder: This rebuilds your widget when there is a state change and can be used to update values on your UI.
BlocBuilder<TestBloc, TestState>(
              buildWhen: (previousState, nextState) {
                return true;
              },
              builder: (context, state) {
                if (state is TestBlocLoading) {
                  return LoadingPage();
                }
                if (state is TestBlocSuccessState) {
                  return LoadingCompletePage();
                }
                if (state is TestBlocFailureState) {
                  return ErrorPage();
                }
                return InitialPage();
              },
            ),

In a case where we want to show different widgets based on the state, blocbuilder is a perfect tool, when no widget is returned for a state it takes the default which in our case is InitialPage.

buildWhen is optional

  1. BlocConsumer:

This is a combination of both BlocBuilder and BlocListener, this is what it'll look like

BlocConsumer<TestBloc, TestState>(
              buildWhen: (previousState, nextState) {
                return true;
              },
              listenWhen: (previousState, nextState) {
                return true;
              },
              listener: (context, state) {
                if (state is TestBlocLoading) {
                  showLoader();
                }
                if (state is TestBlocSuccessState) {
                  Navigator.pushNamed(context, "next_route");
                }
                if (state is TestBlocFailureState) {
                   showToast(error: state.error);
                }
              },
              builder: (context, state) {
                if (state is TestBlocLoading) {
                  return LoadingPage();
                }
                if (state is TestBlocSuccessState) {
                  return LoadingCompletePage();
                }
                if (state is TestBlocFailureState) {
                  return ErrorPage();
                }
                return InitialPage();
              },
            ),

so it is ideal to only use BlocListen when you want to only call functions on your UI and use BlocBuilder if you'll need to rebuild and use BlocConsumer if you'll need both.

I hope it was a nice read, feel free to drop your questions or contributions.

read more about bloc

read more about Testing your bloc

read more about Equatable