What is Flutter BLoC?

Share

What is Flutter BLoC?

BLoC is a popular design/architectural pattern used in software development to design and develop applications.

Android and iOS developers highly popularized the MVC model. The pattern involves the Model holding the data type, the View displaying the data from the Model, and the Controller standing in-between to manipulate and control both.

Flutter brought a new design pattern upon its inception, a variation of this MVC called BLoC.

BLoCBusiness Logic Component uses the concept of Streams, or Reactive programming. This means that when a stream of events is created, subscribers can listen in on the stream of events. The subscribers are notified of new data when it is emitted into the stream.

Widgets in Flutter use this stream for communication and sending data.

  • Stream is setup by BLoC.
  • Widgets subscribes/listens on the BLoC.
  • A Widget sends/emits events to the BLoC.
  • Widgets subscribed to the BLoC are notified of the event and the data emitted.

Stream is of great benefit to the developers because it decouples the business logic from the UI, so that the code is easier to maintain, read, and test.

This design pattern helps separate presentation from business logic, making your code fast, easy to test, and reusable.

Core concepts

There are two concepts of the BLoC pattern we need to understand:

  • Cubit
  • Bloc

Cubit

Cubit is a class that extends Blocbase; this class is used to set state and trigger state changes.

A Cubit represents a part of your application state; so, we can give a state to manage, and the Cubit will expose methods to us that we will use to make changes to the state.

To create a Cubit, extend the Cubit class:

class Score extends Cubit<int> {
    Score(): super(0);
}

The above class, Score, extends the Cubit class – we have an initial state of 1. Initial states are set in Cubit via the super method call. Cubit can manage the state of any data type. In our example, our state is of the Int type, and the initial state is 0.

Cubit has a state property; this property is the current value of our state in the Cubit. So, we can create an instance of our Score cubit and access the state from the state property:

final _score = Score();
_score.state; // 0

Let’s add methods we can use to make changes to the state:

class Score extends Cubit<int> {
    Score(): super(0);

    void addScore(score) => emit(state + score);
}

Cubit has the emit method, used to output or emit a new state. This emitted state becomes the current state of the Cubit. Let’s say the initial state is 9 and emit(6) is called. The current state will now be 6, not 9.

So, the addScore method takes a score that adds to the current score state as arg. The current state and the passing score are added together, and the result of the addition is emitted as the new state via the emit() method.

final _score = Score();
_score.state // 0

_score.addScore(3)
_score.state // 3

_score.addScore(4)
_score.state // 7

Whenever a state is being changed in a Cubit, an onChange method is called before the state change is made. The onChange method is called with the current state and the next state.

class Score extends Cubit<int> {
    Score(): super(0);

    void addScore(score) => emit(state + score);

    @override
    void onChange(Change<int> change) {
        super.onChange(change);
        print(change);
    }
}

Whenever the addScore is called to change the state, the onChange method will be called before the state is changed. So, we will see a log on our console.

Bloc

Bloc is similar to Cubit, but it uses events to do all its workeven to add a new state to itself!. Like Cubit, the state is held in the Bloc, then to change or add new state, events are added to the Bloc.

When an event is added to the Bloc, the onEvent method is called. After this onEvent method is called, a transform events method is called. This method can be used to manipulate the events in the Bloc.

A method mapEventToState is called next with the event. This method then yields the state. This yielded state becomes the new state of the Bloc.

Let’s transform our above Score cubit example to Bloc:

enum ScoreEvent { addScore }

class Score extends Bloc<ScoreEvent, int> {
    Score(): super(0);

    void addScore(score) => emit(state + score);

    @override
    Stream<int> mapEventToState(ScoreEvent event) async* {
        switch(event) {
            case ScoreEvent.addScore:
                yield state + 1;
                break;
        }
    }
}

To use it, do:

final _score = Score();
_score.state; // 0
_score.add(ScoreEvent.addScore);

This will emit ScoreEvent.addScore to the Bloc. The mapEventToState method will be called, and the state in the Score bloc will be incremented to 1.

_score.state; // 1

BlocBuilder

The above sections showed us how to use streams in Dart. Now, we will see how to use them in Flutter.

In Dart, streams of data are created using the Stream object, and we have the:

  • StreamController to control what is emitted to the Stream and close it when done.
  • StreamSubscription to listen on a Stream.

We also have a widget in Flutter, BlocBuilder. This widget listens on a Stream and rebuilds whenever data is emitted into the stream.

The BlocBluider passed a Bloc and a builder function in its bloc arg. This builder function returns the widget tree of the BlocBuilder. The BlocBuilder listens on the Bloc and rebuilds the widget tree in its builder function when the Bloc changes.

Example:

class ScorePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Score')),
      body: BlocBuilder<Score, int>(
        bloc: Score,
        builder: (context, state) {
            return Center( child: Text("Score: $state"))
        }
      );
    );
  }
}

In the above code, the BlocBuilder widget takes a bloc with the Score bloc as its value. Then, the builder function renders a Text widget.

Next, the BlocBuilder will get the state from the Score bloc and call the builder function to render the widget it returns. The BlocBuilder will then pass the current widget, BuildContext, and the current state of the Score bloc to the builder function.

This will make the widgets in the returned tree able to access the state from the Score bloc. The Text is able to get the score value from the state value and render it.

When the state from the Score bloc changes, BlocBuilder will re-render or re-build the widget tree. This means that BlocBuilder will get the new state from the Score bloc. It will also call its builder function with the current BuildContext instance and the new state in the state. So, the Text widget will display the current score.

Add an event to the BlocBuilder

Remember when we used the add method to add an event to a Bloc in a Bloc? Well, BlocBuilder adds the add method to the context.

So, from the context, we call the add method and pass in the event we want to emit. This will change the Bloc state, and BlocBuilder will re-build its tree.

class ScorePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Score')),
      body: BlocBuilder<Score, int>(
        bloc: Score,
        builder: (context, state) {
            return Center( child: Column( children: [Text("Score: $state"), FlatButton( child: Text("Add Score"), onPressed: () {
                context.read<Score>().add(ScoreEvent.addScore),
            })]))
        }
      );
    );
  }
}

The context.read<Score>().add(ScoreEvent.addScore) call will change the state in the Score bloc. Then, the BlocBuilder will re-build the widget tree so that the Text widget will display the new value of the state.

Conclusion

Here, we demonstrated the awesome power of the BLoC pattern and how it can be used in a Flutter app.

The BLoC pattern is reactive and decouples the UI logic from the business logic for it to be developed independently. The BLoC pattern makes our code easier maintain, scale, and test.