BLoC with Freezed: Immutable State & Union Types Made Easy
Last Sync: Today
flutter
BLoC with Freezed: Immutable State & Union Types Made Easy
Introduction
In BLoC, state classes should be immutable to ensure predictability and avoid unintended mutations. Writing immutable state classes manually leads to a lot of boilerplate: constructors, copyWith, ==, hashCode, and serialization code. Freezed is a code generator that creates all of this for you, and it also supports union types (sum types), making it perfect for representing different states like Loading, Success, Error. This guide covers how to integrate Freezed with your BLoC architecture.
Why Use Freezed with BLoC?
Immutability – Enforces immutable state by design.
Automatic copyWith – Update only specific fields without writing boilerplate.
Value Equality – Correct == and hashCode implementations for reliable state comparison.
Union Types – Model states as sealed classes (e.g., LoadingState, DataState, ErrorState).
JSON Serialization – Built‑in toJson/fromJson for persistence (e.g., with hydrated_bloc).
Reduced Boilerplate – Focus on what matters, not repetitive code.
Union types allow you to represent mutually exclusive states. For example, a typical data fetching state can be one of initial, loading, success, or error. Freezed makes this very clean.
Freezed generates helper methods to check the state type (isInitial, isLoading, etc.) and to access data safely (maybeMap, maybeWhen).
DARTRead-only
1
classPostsBlocextendsBloc<PostsEvent, PostsState>{PostsBloc():super(const PostsState.initial()){
on<FetchPosts>(_onFetchPosts);}
Future<void>_onFetchPosts(FetchPosts event, Emitter<PostsState> emit) async {emit(const PostsState.loading());try{
final posts =await api.fetchPosts();emit(PostsState.success(posts));}catch(e){emit(PostsState.error(e.toString()));}}}// In UI, you can use `when` or `map` to handle different states
BlocBuilder<PostsBloc, PostsState>(builder:(context, state){return state.when(initial:()=>constCenter(child:Text('Press button')),loading:()=>constCenter(child:CircularProgressIndicator()),success:(posts)=> ListView.builder(...),error:(message)=>Center(child:Text('Error: $message')),);},);
Adding JSON Serialization
Freezed integrates with json_serializable to generate toJson and fromJson methods. This is especially useful for hydrated_bloc to persist states.
DARTRead-only
1
import'package:freezed_annotation/freezed_annotation.dart';
part 'settings_state.freezed.dart';
part 'settings_state.g.dart';
@freezed
classSettingsStatewith _$SettingsState {const factory SettingsState({
required ThemeMode themeMode,
required String language,})= _SettingsState;
factory SettingsState.fromJson(Map<String, dynamic> json)=>_$SettingsStateFromJson(json);}// Then in your BLoC, extend HydratedBloc and use toJson/fromJsonclassSettingsBlocextendsHydratedBloc<SettingsEvent, SettingsState>{SettingsBloc():super(constSettingsState(themeMode: ThemeMode.system,language:'en'));
@override
SettingsState?fromJson(Map<String, dynamic> json)=> SettingsState.fromJson(json);
@override
Map<String, dynamic>toJson(SettingsState state)=> state.toJson();}
Advanced: Recursive & Generic States
Freezed supports recursive data structures and generics. For example, a tree structure or a generic state wrapper.
Use union types for mutually exclusive states – Models like initial/loading/success/error are a perfect fit.
Keep states immutable – Freezed enforces this; never mutate fields after creation.
Leverage copyWith – Use it to update only the fields that changed.
Combine with hydrated_bloc – Freezed’s JSON serialization makes persistence easy.
Use when or map in UI – This ensures you handle all possible state variants (exhaustiveness).
Name constructors descriptively – initial, loading, success, error improve readability.
Common Mistakes
❌ Forgetting to run build_runner – Results in missing generated files.
✅ Run flutter pub run build_runner build --delete-conflicting-outputs after changes.
❌ Using mutable fields inside a Freezed class – Defeats immutability.
✅ Keep all fields final and use copyWith to create new instances.
❌ Not using @immutable – Freezed doesn’t add it automatically, but it’s good practice to add it.
✅ Add @immutable annotation.
❌ Ignoring union type exhaustiveness – In when, you must handle all cases; this is a feature, not a bug.
✅ Use maybeWhen if some cases can be ignored.
❌ Using @freezed on classes that already have generics without proper configuration – Can cause issues.
✅ Read the Freezed documentation for advanced cases.
Conclusion
Freezed is a game‑changer for writing immutable, self‑documenting state classes in BLoC. It eliminates boilerplate, adds value equality, and enables union types that perfectly match the states of your BLoC. By integrating Freezed with hydrated_bloc and json_serializable, you can build robust, maintainable, and testable apps with minimal code.
What does Freezed generate for your state classes?
A
copyWith, ==, hashCode, toString
B
Only copyWith
C
Only JSON serialization
D
Only union types
Q2
of 3
How do you represent a state that can be loading, success, or error using Freezed?
A
With separate classes
B
With a single class and nullable fields
C
With union types (sealed classes)
D
With enums
Q3
of 3
What command do you run to generate Freezed code?
A
flutter run
B
flutter pub get
C
flutter pub run build_runner build
D
freezed generate
Frequently Asked Questions
Do I need to use Freezed for all BLoC states?
No, but it’s highly recommended because it enforces immutability and reduces boilerplate. For very simple states, you could use a plain class with Equatable, but Freezed scales better as your app grows.
Can I use Freezed with Cubit?
Yes, Freezed works exactly the same with Cubit. The state classes are independent of the state management approach.
How do I handle nested Freezed classes?
Freezed handles nested objects automatically if those objects also have copyWith and == (they can be Freezed classes themselves).
What is the difference between `@freezed` and `@unfreezed`?
@freezed generates a sealed class with union types. @unfreezed generates a plain class with copyWith, ==, etc., but without union types.
How do I migrate an existing BLoC state to Freezed?
Replace your state class with a Freezed‑annotated class, define all required factory constructors, and update your BLoC to use copyWith and union type checks. Then run build_runner to generate the code.