Flutter Bloc v8.0 is Here and Will Change the Way You Handle State Forever
By: Anthony Oleinik
Tags: Flutter, Programming, Mobile App Development, Startup, Code

Cubit was the first fix: remove events completely — now, instead of calling Bloc.add(Event) , you can now call a method on the cubit like cubit.method , allowing us to forgo events completely, cutting boilerplate down by a third.

Cubit wasn’t a perfect cure, though: events were still incredibly helpful and allowed for extensions or monitoring. In my applications, I use cubits 90% of the time — the other 10% I need something more feature rich.

Here’s an example of when you can’t use cubit (psuedocode):

class FooState extends State<Foo> {

final ScrollController _scrollController = ScrollController(); initState() {

super.initState();

_scrollController.addListener(_infiniteScroll);

} void _infiniteScroll() {

if (_scrollController.atBottom()) {

cubit.fetchMorePosts();

}

}

}

We’ve got a real problem here. if a user overscrolls, (which happens almost every time) the scroll controller will say it’s at the bottom…. over, and over, and over, and over again. Depending on scroll distance, you could end up calling fetchMorePosts 100+ times! This isn’t what we want; just call it once and wait.

You could potentially add state to the cubit, like:

class MyCubit extends Cubit<PostState> {

bool isFetching = false; Future<void> fetchMorePosts() {

if (isFetching) return;

isFetching = true;

final newPosts = await _fetch();

isFetching = false;

emit(PostState(newPosts));

}

}

But this leads to a lot more code maintenance and a hell of a lot of debugging. A much simpler way to handle this? Use a bloc with transformEvents.

@override

Stream<FetchEvent> transform(Stream<FetchEvent> events) {

return (events as Observable<FetchEvent>)

.debounce(Duration(milliseconds: 500));

}

Boom — overscroll problem solved.

Bloc also allows us to maintain higher levels of observability, namely by being able to observe input events, not just output states.

Enter v8

Okay, so it’s clear that Bloc is still useful, and we shouldn’t drop it for Cubit entirely. Events are useful for observability, and have good usecases.

We should limit boilerplate as much as possible, but from blocs conception, there’s been one part of the bloc boilerplate that has gone untouched… until v8.

mapEventToState .

mapEventToState has nearly the same exact code in every single bloc:

@override

Stream<State> mapEventToState(Event event) async* {

if (event is EventA) {

yield* _mapEventAToState(event);

} else if (event is EventB) {

yield* _mapEventBToState(event);

}

}



Stream<State> _mapEventAToState(EventA event) async* {...}

Stream<State> _mapEventBToState(EventB event) async* {...}

If you’re unfamiliar with bloc, mapEventToState is a function that takes an input event, and outputs states! I’d say it’s named pretty aptly.

Regardless of apt naming, though, more code = more chances to make mistakes, and there’s a lot of places to make mistakes here. Streams are notoriously difficult concepts, a step above async-await, and require programming finesse to handle elegantly. The stupidly long if else if else if else got too difficult to maintain with many events, and it became easy to lose events in the weeds.

The fix? The new on<Event> method.

on<Event> method of bloc has a single required parameter: a callback with signature FutureOr<void> Function<Event>(Event, Emitter<State>) handler . Breaking it down, on<Event> takes a method that:

is passed two parameters: the input event, and an emitter. If you’ve used cubit , you’ve probably familiar with emitters (If not, no worries! It’ll become clear soon).

, you’ve probably familiar with emitters (If not, no worries! It’ll become clear soon). can be async or sync — doesn’t matter (that’s the FutureOr part!)

you may only call on<Event> once per event type else it will throw a state error. it’s best to do it in the constructor, and then forget about it.

Currently, v8 is backwards compatible with v7 as long as you use version ` 7.2.0-dev.1 . Here’s what that would look like in pubspec.yaml :

bloc: ^7.2.0-dev.1

NOTE: this is a dev release! While it’s been quite stable in my testing, if you’re running a production app, you might want to wait on the migration to hit stable so that you’re in the most bug-free state to avoid unintended behavior.

v8 in Action

Here’s what the classic counter example would look like:

abstract class CounterEvent {} class Increment extends CounterEvent {} class Decrement extends CounterEvent {} class SetCounter extends CounterEvent {

final int setTo; SetCounter({required this.setTo});

} class CounterBloc extends Bloc<CounterEvent, int> {

CounterBloc() : super(0) {

on<Increment>((event, emit) => emit(state + 1));

on<Decrement>((event, emit) => emit(state - 1));

on<SetCounter>((event, emit) => emit(event.setTo));

}

}

no mapEventToState in sight! the constructor sets up a bunch of “listeners”, that listen to the input event. when an input event is given, it calls the callback.

It just so happens, though, that the client for my counter app also wants a “Random” feature. Here’s what he wants me to build: The user can specify a range, and a random integer will be selected from that range. He wants the integer to be gotten from a random integer API he had his microservice team build, so we need to make an API call. (Silly management — don’t they know about dart:math library?)

First, we need to define a state:

class Random extends CounterEvent {

final int from;

final int to; Random({required this.from, required this.to});

}

Easy as that. We won’t code up the display layer, but it’s easy to call with context.read<CounterBloc>().add(Random(from: 10, to: 30)); . It’s also easy to listen to the state with context.watch<CounterBloc>().state .

Next, we need a loading state, because we want the user to know we’re generating a number. So lets write up some states:

abstract class CounterState {} class CounterValue extends CounterState {

final int value;



CounterValue(this.value);

} class CounterLoading extends CounterState {}

Two simple states; one with our value, and one that’s loading.

Now, we need to rewrite our bloc like the following:

class CounterBloc extends Bloc<CounterEvent, CounterState> {

CounterBloc() : super(CounterValue(0)) {

on<Increment>((event, emit) => emit(state + 1));

on<Decrement>((event, emit) => emit(state - 1));

on<SetCounter>((event, emit) => emit(CounterValue(event.setTo)));

}

}

Uh oh, errors! lets rewrite increment and decrement into a private helper functions (just like you’d do in mapEventToState , but this time we don’t need a long if-else chain to find the right event):

CounterBloc() : super(CounterValue(0)) {

on<Increment>((event, emit) => _addToValue(1, emit));

on<Decrement>((event, emit) => _addToValue(-1, emit));

on<SetCounter>((event, emit) => emit(CounterValue(event.setTo)));

} void _addToValue(int toAdd, Emitter<CounterState> emit) {

if (state is CounterValue) {

emit(CounterValue((state as CounterValue).value + toAdd));

}

}

now, if we’re loading we do nothing. if we’re not loading, we perform the required addition / subtraction method! nearly boilerplate free.

Almost done… lets do the random method:

Future<void> _randomize(Random event, Emitter<CounterState> emit) async {

emit(CounterLoading());



final int randomInt = await callApi(event.from, event.to); emit(CounterValue(randomInt));

}

This is state management at it’s finest: while the API call is calling, the user will see a loading state. When the call completes, the user will then see the generated random integer. The magic of bloc, now in easy to read methods!

Finally, lets register the handler. Thankfully, the method we just wrote has the same exact signature as the on<Event> function, so we can just register it like this: on<Random>(_randomize);

That’s it! We’re done. We have the magic of bloc without having to deal with streams, or mapEventToState . We reduced a large chunk of boilerplate and still have responsive, testable state.

Here’s the full code, minus the api call function:

abstract class CounterEvent {} class Increment extends CounterEvent {} class Decrement extends CounterEvent {} class SetCounter extends CounterEvent {

final int setTo;



SetCounter({required this.setTo});

} class Random extends CounterEvent {

final int from;

final int to;



Random({required this.from, required this.to});

}

abstract class CounterState {} class CounterValue extends CounterState {

final int value; CounterValue(this.value);

} class CounterLoading extends CounterState {} class CounterBloc extends Bloc<CounterEvent, CounterState> {

CounterBloc() : super(CounterValue(0)) {

on<Increment>((event, emit) => _addToValue(1, emit));

on<Decrement>((event, emit) => _addToValue(-1, emit));

on<SetCounter>((event, emit) => emit(CounterValue(event.setTo)));

on<Random>(_randomize);

} void _addToValue(int toAdd, Emitter<CounterState> emit) {

if (state is CounterValue) {

emit(CounterValue((state as CounterValue).value + toAdd));

}

} Future<void> _randomize(Random event, Emitter<CounterState> emit) async {

emit(CounterLoading()); final int randomInt = await callApi(event.from, event.to); emit(CounterValue(randomInt));

}

}

Isn’t reactive, testable state beautiful?