Lewis Cianci I'm a passionate mobile-first developer, and I've been making apps with Flutter since it first released. I also use ASP.NET 5 for web. Given the chance, I'll talk to you for far too long about why I love Flutter so much.

Introduction to Flutter BLoC 8

7 min read 1971

Introduction To Flutter BLoC 8

Flutter is a comparatively new cross-platform software development framework with an incredible amount of high-quality, well-supported open sourced packages released during its short lifespan.

One area of Flutter that these packages support is state management, and BLoC is one of the oldest forms of state management within Flutter, originally released to the public towards the end of 2019.

Occasionally, as Flutter improves over time, the flutter_bloc library evolves alongside it.

This trend has continued, and with the latest release of flutter_bloc, there are some breaking changes that require users to upgrade some of the code within existing Flutter apps by hand.

The flutter_bloc 8 breaking changes

It’s hard to update a package and find it has a slew of migration requirements, and worse, that you must perform these migrations by hand, meaning you can’t use any tools to automatically do it for you.

It’s code maintenance in its ugliest format: fixing problems we feel we didn’t create. Surely, it’d be better if the maintainers of flutter_bloc just left things alone and only implemented improvements that meant we didn’t need to do anything, right?

Normally, that’s how I feel. And sometimes I’ve had to migrate between a version for a breaking change and it didn’t feel worthwhile.

However, I can safely say that the changes with flutter_bloc are worthwhile and drastically improve the functionality and stability of what was already a great offering. For a brief summary of the changes, and why they are an improvement to what we have today, check out my video.

These changes make it easier to work with streams and enable apps to work in a more consistent and reliable manner. Let’s dive into what these changes look like and how they’ll affect you.

Previously using mapEventToState

The way that flutter_bloc implements the BLoC method of state management is very simple: events come in and states come out, meaning we send events into our BLoC and yield any range of states (like loaded, success, or failure).

The way this worked in flutter_bloc 7 was like the following:

enum CounterEvent { increment, decrement }

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

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

Here, our BLoC is just a Stream<int>, meaning it can continually emit new values over time. And, because we’re in the CounterBloc, which extends Bloc<CounterEvent, int>, we can access the state variable.

This lets us respond to our events to increment or decrement appropriately.

After a receiving event, the state changes, and a new int yields, seeing this change occur within our UI.

Central to this functionality is the mapEventToState function, which receives events and yields new states. This is the function that deprecated in flutter_bloc 8.0.0, so it’s easy to see that this is a central change to the package.



So, what’s wrong with mapEventToState?

Issues with mapEventToState

First, mapEventToState is very long. We’ve taken 17 lines to accommodate a simple counter BLoC. More complex pieces of code are obviously longer than this, and as the length of our BLoCs grows, the readability of our code begins to suffer.

Secondly, mapEventToState returns a Stream<T>. There’s a good chance that within our BLoCs we’ll want to call other functions that also return streams, and then we must plumb the returned values from our functions into our BLoC.

This is done using a yield generator function (or in Dart, they’re the functions marked as async*). It’s not impossible to use these, but new and seasoned users alike get tripped up when they don’t work as intended.

And finally, there’s actually a very small timing problem with Dart, which affects how streams work. It’s a long story, but all we need to be concerned with is that in flutter_bloc 8, the new implementation doesn’t use streams, so it’s not affected by this bug anymore.

So let’s look at how the release of flutter_bloc 8 resolves these issues.

flutter_bloc introduces event handlers

With the introduction of event handlers, our BLoC code now looks more like this:

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

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

Let’s call out the obvious changes:

First, mapEventToState is gone. Instead, our events register as event handlers. We respond to these events not by yielding a new state, but by instead calling emit with our new state.

And secondly, it’s a lot shorter. Instead of taking up 17 lines to do this, we only take 10. That’s almost a reduction by half, which improves code readability.


More great articles from LogRocket:


Not pictured here is the improvement that we’ll have by using streams. We’ll cover that in our next example.

Converting the old BLoC pattern to work with flutter_bloc 8

Now, let’s create a rudimentary app that uses the old BLoC pattern and convert it to make it work with flutter_bloc 8.0.0. Along the way, we’ll also see how the new BLoC pattern makes our lives easier.

The app is borderline ugly, but it’ll help us understand this new way of doing things.

Final BLoC Pattern Flutter App With The Long-Running Operation, Streamed Operation, And The Long-Running Streamed Operation

Creating an app with flutter_bloc 7

What we usually accomplish within our BLoC is one of two things: either executing a long-running operation that uses await to get a return value, or executing a long-running operation that uses a Stream<String> or Stream<T> as a result, usually for simple progress updates.

Within our BLoC, the code that accommodates these types of requests looks like this:

@override
Stream<HomeState> mapEventToState(event) async* {
  if (event is LoadHomeEvent) {
    yield HomeLoadedState();
  }
  if (event is RunLongRunningEvent) {
    yield HomeLoadingState('Running long running operation....');
    final response = await _fakeNetworkService.longRunningOperation();
    yield HomeLoadingState(response);
    await Future.delayed(Duration(seconds: 2));
    yield HomeLoadedState();
  }
  if (event is RunLongRunningStreamedEvent) {
    yield HomeLoadingState('Running long running streamed operation....');
    yield* _fakeNetworkService.longRunningStream().map((event) => HomeLoadingState(event));
    yield HomeLoadedState();
  }
  if (event is RunLongRunningStreamedComplexEvent) {
    yield HomeLoadingState('Running long running streamed operation with complex objects....');
    yield* _fakeNetworkService.longRunningComplexStream().map(
          (event) => HomeLoadingState(event.message, icon: event.icon),
        );
    yield HomeLoadedState();
  }
}
<

Let’s break this code down and understand how it works.

Long-running operations with await

The code for a long-running operation looks like the following:

if (event is RunLongRunningEvent) {
  yield HomeLoadingState('Running long running operation....');
  final response = await _fakeNetworkService.longRunningOperation();
  yield HomeLoadingState(response);
  await Future.delayed(Duration(seconds: 2));
  yield HomeLoadedState();
}

This is simple enough; our control flow follows this sequence:

  1. Immediately yields our HomeLoadingState because a request is in progress
  2. Uses our fake long-running operation and awaits the result
  3. Yields the response when it arrives
  4. Waits another 2 seconds (so the user can read the message)
  5. Finally yields the HomeLoadedState

This is the most trivial implementation of yielding asynchronous data within our BLoC. Let’s see how things get more complicated when we introduce streams.

Long-running operations with Stream<String>

Sometimes our functions yield over time instead of returning a single value. It’s also possible we’ve implemented this ourselves to report on the progress for an individual component in our application. In this case, our BLoC looks like the following:

if (event is RunLongRunningStreamedEvent) {
  yield HomeLoadingState('Running long running streamed operation....');
  yield* _fakeNetworkService.longRunningStream().map((event) => HomeLoadingState(event));
  yield HomeLoadedState();
}

However, the control flow for this is a bit tricker. Let’s step through it.

First, we immediately yield our HomeLoadingState because a request is in progress. Then, by using a yield generator function (the yield* function) to connect to a stream within our service, we can plumb the stream output into our existing BLoC stream.

Because our BLoC returns Stream<HomeState> and our service returns String, we must use the .map operator to convert from the services’ data type to our BLoC’s data type.

Finally, we can yield the HomeLoadedState.

Now, how many times did you have to reread the second step here? Did your eyes glaze over a little bit? And what on earth is a yield generator function? Should I be concerned?

If you feel like that, you have every right. Connecting streams in this manner is confusing and easy to get wrong, and if you do get it wrong, your BLoC will just hang stop forever and never complete.

Newcomers and seasoned developers get this frequently wrong, and it’s a frustrating problem to fix.

I’ve also included an example for mapping complex objects, that is, a class we’ve made ourselves. It’s largely the same as the Stream<String> example using the map operator and the yield generator function.

Fortunately, this workflow has improved significantly in flutter_bloc 8.0.0. Let’s see how by migrating this code to the newer version of flutter_bloc.

Migrating our code to flutter_bloc 8.0.0

The first thing we must do is upgrade our flutter_bloc package in our pubspec.yaml to 8.0.0. It should look like this:

Upgrading The flutter_bloc Package In The CLI

And now, we can start migrating our code. Let’s hop back into our home_bloc.dart and move our code over to the ✨new way✨ of doing things.

Long-running operations with await in flutter_bloc 8

Because we have no more mapEventToState, we must now set up event handlers and use our events as the types of events that we register to listen to. For our first example, our BLoC now looks like this:

on<RunLongRunningEvent>((event, emit) async {
  emit(HomeLoadingState('Running long running operation...'));
  final response = await _fakeNetworkService.longRunningOperation();
  emit(HomeLoadingState(response));
  await Future.delayed(Duration(seconds: 2));
  emit(HomeLoadedState());
});

We’re still awaiting our service, but instead of calling yield, we’re using the emit function that passes in to emit these new states into our BLoC.

Where we really start to benefit from this new methodology is when we subscribe to long-running streams, so let’s look at that now.

Long-running operations with Stream<String> and Stream<T>

Remember how we had our yield generator function and things were pretty confusing in the mapEventToState days? This is how it looks after migrating our code to the new way of doing things:

on<RunLongRunningStreamedEvent>((event, emit) async {
  emit(HomeLoadingState('Running long running streamed operation...'));
  await for (final result in _fakeNetworkService.longRunningStream()) {
    emit(HomeLoadingState(result));
  }
  emit(HomeLoadedState());
});

We can use await for to emit new states as our Stream serves them up. We don’t need to use the yield generator function, and our control flow for this part of our BLoC makes more logical sense. Similar benefits are realized in the stream that uses a complex class:

on<RunLongRunningStreamedComplexEvent>((event, emit) async {
  emit(HomeLoadingState('Running long running streamed complex operation...'));
  await for (final result in _fakeNetworkService.longRunningComplexStream()) {
    emit(HomeLoadingState(result.message, icon: result.icon));
  }
  emit(HomeLoadedState());
});

Again, we use our await for method here to receive events and send them into our BLoC. Not needing to yield new states or muck around with yield generators makes this a lot better.

Conclusion

So, the next version of flutter_bloc is poised to make it easier for you to make apps in Flutter. It has some breaking changes, which you must migrate by hand, but the end result is well worth it.

All the code you saw today is available here, and I’ve tagged the commits in GitHub with bloc-v7 and bloc-v8, respectively. You can change between commits at your leisure to see how the code changed between each version of flutter_bloc.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Lewis Cianci I'm a passionate mobile-first developer, and I've been making apps with Flutter since it first released. I also use ASP.NET 5 for web. Given the chance, I'll talk to you for far too long about why I love Flutter so much.

Leave a Reply