Peter Bryant
Peter Bryant

Peter Bryant

Why Bloc + Freezed is a match made in heaven

Peter Bryant's photo
Peter Bryant
·Apr 22, 2022·

5 min read

Why Bloc + Freezed is a match made in heaven

Photo by Alexandre Boucey on Unsplash

Subscribe to my newsletter and never miss my upcoming articles

I'm a big fan of the Bloc library for Flutter apps, and when coupled with the freezed package for creating immutable data classes and sealed unions, I think this pattern really shines.

Sealed whats?

Let's start with some definitions. In other languages (like Kotlin), there are patterns available such as union types, sealed classes, and pattern-matching.

These are all slightly different expressions of the same idea: that you can create a class with a fixed set of subclasses, linking together multiple otherwise separate types under one "umbrella" class.

Shapes are a good example of how this could be used. Here's an example in Kotlin:

sealed class Shape

data class Square(val sideLength: Int): Shape()

data class Rectangle(val length: Int, val width: Int): Shape()

data class Circle(radius: Int): Shape()

Here we are defining 3 classes - Square, Rectangle, and Circle. Each has its distinct properties, but we're able to have them all extend the Shape superclass.

More importantly, because Shape is a sealed class, no other subtypes can be defined outside this file; in other words, we can restrict the subtypes of Shape to the ones we define.

So why would this be helpful when using the Bloc pattern?

Let's consider a simple Counter bloc. It would have two Events: CounterIncremented and CounterDecremented. In pure Dart, we would have to do something like this:

abstract class CounterEvent {}

class CounterIncremented extends CounterEvent {
  CounterIncremented(this.incrementBy);

  final int incrementBy;
}

class CounterDecremented extends CounterEvent {
  CounterDecremented(this.decrementBy);

  final int decrementBy;
}

This is fine, but there are a couple of noteworthy issues:

  • For Blocs with lots of events, you can see how this approach would become very verbose and cumbersome.
  • The above snippet doesn't take equality operators into account; that's more boilerplate we'd need to add.
  • When we eventually implement this Bloc, our code will be full of type-casting and type-checking, which is easy to make a mess with.

If we were writing Kotlin, we could solve these problems by implementing CounterEvent as a sealed class with each event type as a data class extending the base class.

sealed class CounterEvent

data class CounterIncremented(val incrementBy: Int) : CounterEvent()

data class CounterDecremented(val decrementBy: Int) : CounterEvent()

Kotlin's data class construct gives us immutability and equality without boilerplate.

But we're not writing Kotlin, and Dart doesn't have support for sealed classes out-of-the-box. We need to find an alternative!

Freezed Events

The freezed package gives us a couple of important abilities: we can create immutable data classes, and we can create sealed unions. Perfect!

Here's what our Counter events would look like using a freezed union:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'counter_event.freezed.dart';

@freezed
class CounterEvent with _$CounterEvent {
    const factory CounterEvent.incremented(int incrementBy) = CounterIncremented;
    const factory CounterEvent.decremented(int decrementBy) = CounterDecremented;
}

With this implementation, freezed has given us two immutable event types with equality operators. It even generates bonus stuff like copyWith and toString methods. This is a totally battle-ready implementation of our CounterEvent type.

When it comes to actually using instances of CounterEvent, freezed also gives us access to some pattern-matching syntax. Here's how we could implement our event handler:

event.when(
    incremented: (incrementBy) => emit(state + incrementBy),
    decremented: (decrementBy) => emit(state - decrementBy),
);

This syntax is helpful for a couple of reasons. First, forces us to account for every possibility; in pure Dart with type-casting, we could easily forget about or ignore certain event types. Second, it's super easy to read and understand!

Freezed States

Let's look at something more complex: writing a state type using freezed. Let's say we have some data to load: we'll probably want an initial state, an error state, and a success state. We also need a loading indicator to show the user when the data is being refreshed.

Using freezed, we can implement a state class like this:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'data_state.freezed.dart';

@freezed
class DataState with _$DataState {
    const factory DataState.initial(bool isLoading) = DataInitial;
    const factory DataState.error(String message, bool isLoading) = DataError;
    const factory DataState.success(Data data, bool isLoading) = DataSuccess;
}

Then, we can implement a BlocBuilder like this:

BlocBuilder<DataBloc, DataState>(
    builder: (context, state) {
        return Column(
            children: [
                if (state.isLoading) _buildProgressIndicator(),
                state.when(
                    initial: (_) => _buildInitial(),
                    error: (message, _) => _buildError(message),
                    success: (data, _) => _buildSuccess(data),
                ),
            ],
        );
    },
);

There are a couple of things to notice here:

  • Because every state type has an isLoading property, we can access it without knowing what subtype of state we have. Thanks, freezed!
  • Again, the pattern-matching syntax provided by freezed is extremely useful here in giving us some super-readable code which forces us to handle all possible states.

Is this the perfect solution?

No.

There is no perfect solution! It could very easily be argued that freezed classes are still pretty verbose, and some developers prefer to not rely on code generation in their apps. And some folks just don't like Bloc at all for some reason. That's all fine - do what works for you!

But, having worked on more than 10 new Flutter projects in the past year or so, I've found that this pattern allows us to build and iterate quickly on our code, and improves my team's ability to understand one another's work easily.

As with lots of software engineering patterns and practices, there's no single right way to do things; the most important thing you can do is to pick one pattern and stick to it consistently.

Did you find this article valuable?

Support Peter Bryant by becoming a sponsor. Any amount is appreciated!

See recent sponsors Learn more about Hashnode Sponsors
 
Share this