Flutter: 6 Levels of BLoC State Management

Elias Elfarri
Fink Oslo
Published in
8 min readMay 27, 2024

Are you a Flutter developer that actively uses the flutter_bloc and bloc packages in your Dart/Flutter projects? Then this is the perfect article for you! As the title suggests, I have divided the article into 6 levels of custom state manager code snippets where each level adds to the complexity and shapes the basic idea of what makes the BLoC pattern what it is. By the end of it all you will have the basis of how to implement your own custom bloc pattern state manager.

If you finish one then the fun’s just begun.

Level 1 — The Naive Approach

class StateManager<State> {
//fields
State? _previousState;
late State _currentState;

//constructor
StateManager(State initialState) {
_currentState = initialState;
}

//getters
State get currentState => _currentState;
State? get previousState => _previousState!;

void emit(State newState) {
_previousState = _currentState;
_currentState = newState;
}
}

The naive approach is usually the best way to wrap your head around a complex concept. In this case we are looking at a Dart class that minimally represents and satisfies the definition of a state manager. It has simply the following properties:

  • Generic “State” private member variables, _previousState and _currentState
  • Getters for the private member variables
  • Instantiable class with constructor to set the _currentState
  • An emit class method that mutates the _currentState and keeps track of the _previousState

This state manager is in itself quite useful and just within the scope of a main function seen below, we are able to initialize an int to 0 and increment it by 1.

void main() {
StateManager stateManager = StateManager<int>(0);

print(stateManager.currentState); // 0
stateManager.emit(stateManager.currentState++);
print(stateManager.currentState); // 1
}

This is not to bad but it is quite limited. I would love to be able to write classes that incapsulates all functionality and business logic related to a specific state management concept.

To unwrap what I am blabbering about, think about counting int values from 0 to infinity as we do in the main function. That sounds to me like a Counter! Wouldn’t it be good if we can keep all Counter related state management inside a CounterStateManager class of some kind? This is my attempt at foreshadowing.

If you make it through two, then congrats are due.

Level 2 — Class Inheritance

class CounterStateManager extends StateManager<int> {
CounterStateManager(int initialState) : super(initialState);

void increment() {
emit(currentState + 1);
}

void decrement() {
emit(currentState - 1);
}
}

Using our rather naive implementation of a state manager from Level 1, we can create a more specialized and contextualized version that inherits the properties of the base StateManager class. In short we have:

  • A CounterStateManager class that inherits StateManager base class with the state being an integer
  • Instantiate the CounterStateManager class by passing initialState to the StateManager base class
  • Specific increment and decrement methods that are wrappers for the emit method that mutates the internal state

The main function does not look much different, but we have abstracted away the business logic.

void main() {
CounterStateManager counterStateManager = CounterStateManager(0);

print(counterStateManager.currentState); // 0
counterStateManager.increment();
print(counterStateManager.currentState); // 1
counterStateManager.decrement();
print(counterStateManager.currentState); // 0
}

We are onto something but it is still confusing that you can both instantiate a StateManager class and its children classes.

If you make it to three, a champ you’ll be.

Level 3 — Class Abstraction

//-- NEW --
abstract class StateManager<State> {
//-- NEW --

//fields
State? _previousState;
late State _currentState;

//constructor
StateManager(State initialState) {
_currentState = initialState;
}

//getters
State get currentState => _currentState;
State? get previousState => _previousState!;

void emit(State newState) {
_previousState = _currentState;
_currentState = newState;
}
}

You probably could have guessed it, the only thing we have done from level 2 to 3 is to make the StateManager base class abstract. The reason why we want to do this, is to restrict developers from being allowed to instantiate StateManager, instead they’d have to create subclasses that inherit the StateManager and contains their business logic i.e. CounterStateManager. Simply put, it allows to enforce a specific pattern of usage, and this pattern is directly linked to how you exactly want to enforce the bloc pattern.

If you make it to four, you’re on a winning tour.

Level 4— Composability

//-- NEW --
abstract interface class Emittable<State extends Object?> {
void emit(State newState);
}
//-- NEW --

abstract class StateManager<State> implements Emittable<State> {
//fields
State? _previousState;
late State _currentState;

//constructor
StateManager(State initialState) {
_currentState = initialState;
}

//getters
State get currentState => _currentState;
State? get previousState => _previousState!;

void emit(State newState) {
_previousState = _currentState;
_currentState = newState;
}
}

The code running in the main function level 2 will still run exactly the same after these changes i just present here. Presenting an abstract interface class Emittable that only contains an emit method, for simplicity let us say that it enforces an emit function to be implemented in the StateManager class. And as it seems the method was implemented since level 1, so it doesn’t break anything and everything runs exactly as it has done earlier!

By moving the emit method to the Emittable interface rather than just having it directly in the StateManager class, it promotes a composable structure. Such that it makes it easier to compose different classes that emit state changes without the need for repeating the implementation everywhere. In the case of our 6 levels of state management it will only add additional complexity, much like we have done in level 3. But if you look into the source code of flutter_bloc, after reading this, then what I am walking you through here demystifies certain decisions that have been taken by the authors of the packages.

If you make it to five, then you will thrive.

Level 5— Cubits

//-- NEW --
abstract class Streamable<State extends Object?> {
Stream<State> get stream;
}
//-- NEW --

abstract interface class Emittable<State extends Object?> {
void emit(State newState);
}

abstract class StateManager<State>
implements Emittable<State>, Streamable<State> {
//fields
State? _previousState;
late State _currentState;

//-- NEW --
late final _stateController = StreamController<State>.broadcast();

@override
Stream<State> get stream => _stateController.stream;
//-- NEW --

//constructor
StateManager(State initialState) {
_currentState = initialState;
}

//getters
State get currentState => _currentState;
State? get previousState => _previousState!;

void emit(State newState) {
_previousState = _currentState;
_currentState = newState;

//-- NEW --
_stateController.add(_currentState);
//-- NEW --
}
}

Thanks to the Dart out of the box support for streams we can easily add a stream to our StateManager class and pipe them out to every listener that is interested in the state changes. In the code above, we have added the following to our state manager:

  • The private member stream controller_stateController and instantiated it as a broadcast stream of the generic type State.
  • I add a Stream getter to get the stream out of the streamController in our state manager.
  • To emit new values to all listeners of the stream, every time we emit a new state, we also add the new state to the sink such that the value gets broadcast to listeners of the stream. That’s the point of having _stateController.add(_currentState)
  • Finally we add an interface Streamable which is a composability thing as explained in level 4 for the Emittable class.

On top of this abstract StateManager class we can now create a Cubit! Which is a known class for everyone working with the flutter_bloc package.


abstract class Cubit<State> extends StateManager<State> {
Cubit(State initialState) : super(initialState);
}

The cubit just abstracts away the StateManager implementation details and can be used to create the following CounterCubit:

class CounterCubit extends Cubit<int> {
CounterCubit(int initialState) : super(initialState);

void increment() {
emit(currentState + 1);
}

void decrement() {
emit(currentState - 1);
}
}


Future<void> main() async {
CounterCubit counterCubit = CounterCubit(0);

counterCubit.stream.listen((state) {
print(state);
});

//every half a second emit a new state
await Timer.periodic(
Duration(milliseconds: 500),
(timer) {
counterCubit.increment();

if (counterCubit.currentState == 5) {
timer.cancel();
}
},
);
}

Now as you can see the CounterCubit and what i introduced as CounterStateManager in level 2 are very similar to each other! The only difference is that the Cubit class also can stream state changes to its listeners, but the way the state mutates through business logic methods such as increment and decrement has stayed the same! Now Let us look at level 6 where I introduce the Bloc class.

If you make it through six, you’ll learn some slick tricks.

Level 6— BLoC

abstract class Bloc<Event, State> extends StateManager<State> {
Bloc(State initialState) : super(initialState);

void add(Event event);
}

The Bloc class is some what similar to the Cubit class in that it inherits the properties of the StateManager base class of level 5. Yet what is interesting is that it also has another generic type of Event, as well as a method to add such event classes. There is no other way to truely explain this than to show the code! Let us look at how a CounterBloc class would look like.

abstract class CounterEvent {}
class IncrementEvent extends CounterEvent {}
class DecrementEvent extends CounterEvent {}


class CounterBloc extends Bloc<CounterEvent, int> {
CounterBloc(int initialState) : super(initialState);

@override
void add(CounterEvent event) {
switch (event) {
case IncrementEvent _:
_onIncrement();
break;
case DecrementEvent _:
_onDecrement();
break;
}
}

void _onIncrement() {
emit(currentState + 1);
}

void _onDecrement() {
emit(currentState - 1);
}
}

A CounterBloc class has a State of type int and an Event of type CounterEvent. This CounterEvent are just specific Increment and Decrement Events almost like enum values, but classes. Then we can create a switch statement based on which event we want to run. If it is an increment event then we can call the _onIncrement private method which then calls the emit. You might realize that this is a bit overkill in the case of a counter, hence why cubits exist! But for more complicated matters then the BLoC pattern keeps the code clean through events.

What remains to see is the main function for how we emit new values, instead of using an increment method directly, we call the add function which adds a specific event, in our case an IncrementEvent or a DecrementEvent class.

Future<void> main() async {
CounterBloc counterBloc = CounterBloc(0);

counterBloc.stream.listen((state) {
print(state);
});

//every half a second emit a new state
await Timer.periodic(
Duration(milliseconds: 500),
(timer) {
counterBloc.add(IncrementEvent());

if (counterBloc.currentState == 5) {
timer.cancel();
}
},
);
}

Now you have seen all there is to learn about how to implement a simple custom bloc state manager, that enforces the bloc pattern!

Of course there are few things worth noting from this article:

  • I do not cover how to close/dispose the stream
  • Error handling for the state manager
  • Logging & observers
  • Testing
  • How you connect this to Flutter UI — the easiest way is to do it through the Provider package!

If you’ve made it this far then congratulations! Tap yourself on the back, because you just made it through the 6 levels of how to implement your own custom bloc state manager. Go ahead and unleash your new knowledge onto the world! You can find the github repo with all the code snippets here.

--

--