Better with bloc

Stephan E.G. Veenstra
6 min readAug 22, 2023

In this article I will show you how I use bloc state management in my Flutter projects. The approaches mentioned in this article are based on my personal preferences and coding style.

🚨 This article will assume you have at least some basic knowledge of the flutter_bloc package. For learning on how to get started with flutter_bloc, please refer to the docs.

💡 In this article I will mostly speak of blocs, but these can easily be substituted by cubits.

Bloc feat. Freezed

Freezed is a package that goes very well with flutter_bloc. If you don’t know what freezed is, you are missing out!

️️️️💡 Freezed basically allows you to write super-charged (data) classes. All you have to do is define the properties for your class and freezed can generate things like toString, hashcode, operator == and copyWith methods for you.

The bloc documentation describes two ways of creating state objects. The first one is by using subclasses, where every subclass represents a state the state object can be in.

sealed class CounterState {}
final class CounterInitial extends CounterState {}
final class CounterLoading extends CounterState {}
final class CounterLoaded extends CounterState {
required int count,
}
final class CounterError extends CounterState {}

The second is by using a single class where the different states are represented by the status field.

enum CounterStatus { initial, loading, loaded, error }
final class CounterState {
const CounterState({this.status = CounterStatus.initial});
final CounterStatus status;
final int? count;
}

I prefer the first option. Using subclasses allows me to be mutual exclusive, so I never have to worry if a field of the state can or shouldn’t be accessed. In the single class example, the count field will always be accessible, while it probably only should be read when the state is loaded.

Unfortunately, creating that many subclasses by hand can be quite cumbersome. Specially when they get (and share) a lot of fields.

Lucky for us, there are freezed union types!

Union types

One of the best things of freezed is the support for union types. You can think of a union as a class that can have different representations, just like our state object.

I can create the same state object with freezed, like this:

@freezed
sealed class CounterState with _$CounterState {
const factory CounterState.initial() = CounterInitial;
const factory CounterState.loading() = CounterLoading;
const factory CounterState.Loaded({
required int count,
}) = CounterLoaded;
const factory CounterState.error() = CounterError;
}

It looks very similar to the original example, but under the hood, freezed will generate code that makes our lives so much easier.

Using our state object

Now that we have a nice state object to work with, let’s use it. We are going to implement an increment method for our CounterCubit:

void increment() {
// We have to store the current state into a local variable in
// order for Dart to be able to downcast it.
final currentState = state;
switch(currentState) {
// We can only increment when the state is CounterLoaded
case CounterLoaded(:final count):
emit(currentState.copyWith(count: count + 1));
// Anything else will just be ignored
default:
}
}

As you can see, we’re able to use the generated .copyWith method to easily turn the current state into the new state.

️️ ️ 💡 In this example I’m using the new Dart pattern matching functionality that came with Dart 3. Freezed is also able to generate .map/.when methods for us that can basically do the same. While I prefer the map/when syntax, I’m not using it since it’s been discouraged by the author.

Still, freezed just made our lives a bit… cooler 😎.

Custom listeners

If you ever had to fire off one-off actions based on state changes, like navigating, or showing a snackbar, you will most likely have used a BlocListener.

But when you have to listen to a bunch of different state changes, the code can become quite cluttered.

Let’s take a look at the following example:

Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
// 1. When the user has successfully logged in, navigate to home
BlocListener<LoginBloc, LoginState>(
// Only trigger when state transitions into loginSuccess
listenWhen: (previousState, state) =>
previousState is! LoginSuccess && state is LoginSuccess,
listener: (context, state) => context.go('/home/${state.user.id}'),
),
// 2. Show a dialog when the user account needs to be verified
BlocListener<LoginBloc, LoginState>(
// Only trigger when state transitions into loginSuccessUnverified
listenWhen: (previousState, state) =>
previousState is! LoginSuccessUnverified && state is LoginSuccessUnverified,
listener: (context, state) => context.showVerificationDialog(),
),
],
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
// UI CODE HERE
},
),
);
}

As you can see, there is quite a bit of logic in our UI file and it’s making quite a mess. I also felt the need to add comments to make it more clear to what each listener does.

But, we can easily make this better bij creating our own listeners by extending BlocListener!

class LoginSuccessListener extends BlocListener<LoginBloc, LoginState> {
LoginSuccessListener(
void Function(BuildContext context, LoginState state) listener,
{
super.child,
}) : super(
buildWhen: (previousState, state) =>
previousState is! LoginSuccess && state is LoginSuccess,
listener: listener,
);
}

Doing this has several benefits:

  1. The name of the listener tells you what it does.
  2. The buildWhen logic is nicely tucked away.
  3. The new listener is easily reusable.

If we do the same thing for the other listener, we can clean up our UI like so:

Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
LoginSuccessListener(
(context, state) => context.go('/home/${state.user.id}'),
LoginSuccessUnverifiedListener(
(context) => context.showVerificationDialog()),
],
child: BlocBuilder<LoginBloc, LoginState>(
builder: (context, state) {
// UI CODE HERE
},
),
);
}

That’s a lot better now, isn’t it?

Consuming blocs

Even though blocs are a crucial part of my applications, I don’t want them to be mixed in with all the UI code. Therefor I have one very strict rule:

Only destination widgets should deal with blocs!

Widgets that are the root of a page, screen, dialog, bottomsheet, etc is what I call a destination widget. These are widgets that you use in your router, or your navigation methods, for example:

Navigator.push(context, MaterialPageRoute(builder: (context) => HomePage()));

Here HomePage can be considered a destination widget.

So, only destination widgets deal with blocs. They will orchestrate the content on the screen.

Most of the time I will split up a destination widget into several private widgets that can represent each state. If the file is getting too big for your taste, you could break the file up in their own (part) files.

Let’s take a look at a full example.

class TodoListPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
// We start with the listeners for this page.
return MultiBlocListener(
listeners: [
TodoRemovedListener((context) => context.showTodoRemovedSnackbar()),
TodoAddedListener((context) => context.showTodoAddedSnackbar()),
],
// Then we add the builder.
child: BlocBuilder<TodoListBloc, TodoListState>(
builder: (context, state) => switch(state) {

// Often initial and loading can use the same widget.
TodoListInitial() => _Loading(),
TodoListLoading() => _Loading(),

// When the state is loaded, we want to show the todos.
TodoListLoaded(:final todos) => _Loaded(todos),

// When we are refreshing, it might be nice to keep showing the
// current todos, but we also tell the widget to indicate refreshing.
TodoListRefreshing(:final todos) => _Loaded(todos, isRefreshing: true),

// If loading went wrong, we can show an error.
TodoListError(:final error) => _Error(error),
}
),
),
}
}

class _Loading extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Show loading screen
}
}

class _Loaded extends StatelessWidget {
_Loaded(this.todos, {this.isRefreshing = false});

final List<Todo> todos;
final bool isRefreshing;

@override
Widget build(BuildContext context) {
// Show Todo List and optional a refreshing indicator
// If the todos are empty, maybe even show an empty list message
}
}

class _Error extends StatelessWidget {
_Error(this.error);

final Error error;

@override
Widget build(BuildContext context) {
// Show error screen
}
}

I think this looks pretty tight! 🔥

💡 The reason I limit the amount of widgets that may access blocs, is because this way, I know exactly where things can go wrong. It also allows for all the other, non-destination, widgets to be as ‘dumb’ as possible.

Conclusion

In this article I’ve shown you how I use bloc in my applications.

  • First I talked about the freezed package, and how we can benefit from it in combination with bloc.
  • Second, I showed you what I do to move BlocListener logic out of the UI by creating custom BlocListeners with more descriptive names.
  • Lastly, I explained which widgets, that I call destination widgets, I allow to have access to blocs, and why.

I hope you found this article useful/interesting. If you like more of these please let me know by clapping 👏. If you have specific things you’d like to see, please leave a comment👇! Thanks! 🙏

Continue reading: Part II

--

--

Stephan E.G. Veenstra

When Stephan learned about Flutter back in 2018, he knew this was his Future<>. In 2021 he quit his job and became a full-time Flutter dev.