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:
- Don't miss a moment with The Replay, a curated newsletter from LogRocket
- Learn how LogRocket's Galileo cuts through the noise to proactively resolve issues in your app
- Use React's useEffect to optimize your application's performance
- Switch between multiple versions of Node
- Discover how to animate your React app with AnimXYZ
- Explore Tauri, a new framework for building binaries
- Advisory boards aren’t just for executives. 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.
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.
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:
- Immediately yields our
HomeLoadingState
because a request is in progress - Uses our fake long-running operation and awaits the result
- Yields the response when it arrives
- Waits another 2 seconds (so the user can read the message)
- 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:
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
.
Get set up with LogRocket's modern error tracking in minutes:
- Visit https://logrocket.com/signup/ to get an app ID
- Install LogRocket via npm or script tag.
LogRocket.init()
must be called client-side, not server-side - (Optional) Install plugins for deeper integrations with your stack:
- Redux middleware
- NgRx middleware
- Vuex plugin
$ 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>