Mastering the Freezed Package in Flutter: Benefits, Use Cases, and BLoC Integration
Mastering the Freezed Package in Flutter: Benefits, Use Cases, and BLoC Integration
State management and immutable data structures are at the heart of robust Flutter applications. The freezed package is a powerful code generator that simplifies the creation of immutable classes and union types in Dart. In this guide, we’ll explore what Freezed is, its benefits, and how to use it effectively—especially in combination with the BLoC pattern.
🚀 What is Freezed?
Freezed is a Dart package that generates immutable classes, union types (sealed classes), and provides utilities like copyWith
, deep equality, and pattern matching. It is inspired by Kotlin’s data classes and sealed classes, making Dart code more concise, readable, and less error-prone.
🎯 Why Use Freezed?
- Immutability: Ensures your data models cannot be changed after creation, preventing accidental bugs.
- Union/Sealed Classes: Enables expressive state and event modeling, especially useful in BLoC.
- Automatic
copyWith
: Easily create modified copies of objects. - Deep Equality: Compares objects by value, not reference.
- Pattern Matching: Use
when
andmap
for exhaustive and type-safe handling of unions. - Serialization: Supports JSON serialization with json_serializable.
🛠️ Setting Up Freezed
Add dependencies to your pubspec.yaml
:
dependencies:
freezed_annotation: ^2.4.1
json_annotation: ^4.8.1
dev_dependencies:
build_runner: ^2.4.6
freezed: ^2.4.6
json_serializable: ^6.7.1
✨ Creating Immutable Data Classes
Let’s create a simple User
model:
import 'package:freezed_annotation/freezed_annotation.dart';
part 'user.freezed.dart';
part 'user.g.dart';
@freezed
class User with _$User {
const factory User({
required String id,
required String name,
int? age,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
- Run
flutter pub run build_runner build
to generate the code. - The generated class is immutable, supports
copyWith
, deep equality, and JSON serialization.
Example Usage
final user1 = User(id: '1', name: 'Alice', age: 25);
final user2 = user1.copyWith(name: 'Bob');
print(user1 == user2); // false
🔀 Union Types (Sealed Classes) with Freezed
Union types are perfect for modeling states and events in BLoC.
@freezed
class AuthState with _$AuthState {
const factory AuthState.initial() = _Initial;
const factory AuthState.loading() = _Loading;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.error(String message) = _Error;
}
Pattern Matching
authState.when(
initial: () => print('Initial'),
loading: () => print('Loading'),
authenticated: (user) => print('Welcome ${user.name}!'),
error: (msg) => print('Error: $msg'),
);
🤝 Using Freezed with BLoC
Freezed shines when used with BLoC for modeling events and states.
Example: Counter BLoC
counter_event.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'counter_event.freezed.dart';
@freezed
class CounterEvent with _$CounterEvent {
const factory CounterEvent.increment() = Increment;
const factory CounterEvent.decrement() = Decrement;
}
counter_state.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'counter_state.freezed.dart';
@freezed
class CounterState with _$CounterState {
const factory CounterState({required int value}) = _CounterState;
}
counter_bloc.dart
import 'package:flutter_bloc/flutter_bloc.dart';
import 'counter_event.dart';
import 'counter_state.dart';
class CounterBloc extends Bloc<CounterEvent, CounterState> {
CounterBloc() : super(const CounterState(value: 0)) {
on<Increment>((event, emit) => emit(state.copyWith(value: state.value + 1)));
on<Decrement>((event, emit) => emit(state.copyWith(value: state.value - 1)));
}
}
Benefits in BLoC
- Type Safety: Only valid states/events are possible.
- Pattern Matching: Use
when
/map
for exhaustive handling. - Immutability: Prevents accidental state mutation.
📦 Serialization with Freezed
Freezed integrates with json_serializable
for easy JSON (de)serialization:
final user = User.fromJson({'id': '1', 'name': 'Alice', 'age': 25});
final json = user.toJson();
🧑💻 Advanced Use Cases
1. Nested Models
Freezed supports nested immutable models:
@freezed
class Post with _$Post {
const factory Post({
required String id,
required User author,
required String content,
}) = _Post;
}
2. Default Values
@freezed
class Settings with _$Settings {
const factory Settings({
@Default(true) bool darkMode,
@Default('en') String language,
}) = _Settings;
}
3. Custom Getters and Methods
@freezed
class Rectangle with _$Rectangle {
const factory Rectangle({
required double width,
required double height,
}) = _Rectangle;
const Rectangle._();
double get area => width * height;
}
4. Unions for API Responses
@freezed
class ApiResponse<T> with _$ApiResponse<T> {
const factory ApiResponse.success(T data) = Success<T>;
const factory ApiResponse.error(String message) = Error<T>;
}
🏆 Best Practices
- Always run
build_runner
after editing Freezed classes. - Use
@Default
for default values. - Prefer union types for BLoC states/events.
- Use
copyWith
for immutable updates. - Leverage pattern matching for exhaustive handling.
❓ Common Pitfalls
- Forgetting to run
build_runner
after changes. - Not including
part
files. - Using mutable fields in Freezed classes.
📚 Resources
Happy coding! 🚀