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.
flutter_bloc
8 breaking changesIt’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.
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
?
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 handlersWith 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.
Not pictured here is the improvement that we’ll have by using streams. We’ll cover that in our next example.
flutter_bloc
8Now, 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.
flutter_bloc
7What 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.
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:
HomeLoadingState
because a request is in progressHomeLoadedState
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.
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
.
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:
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.
await
in flutter_bloc
8Because 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.
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.
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
.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowconsole.time is not a function
errorExplore the two variants of the `console.time is not a function` error, their possible causes, and how to debug.
jQuery 4 proves that jQuery’s time is over for web developers. Here are some ways to avoid jQuery and decrease your web bundle size.
See how to implement a single and multilevel dropdown menu in your React project to make your nav bars more dynamic and user-friendly.
NAPI-RS is a great module-building tool for image resizing, cryptography, and more. Learn how to use it with Rust and Node.js.