Guide to Understand and Use Freezed Package in Flutter

Learn how to leverage the Freezed package in Flutter/Dart to generate immutable data classes with JSON serialization, copyWith, unions, and more—cutting boilerplate and boosting type safety.
@shadcn

Stuti Gupta

a month ago

guide-to-understand-and-use-freezed-package-in-flutter

1. Introduction

What is Freezed?
A powerful code‑generation package that removes boilerplate from your Dart/Flutter models. With Freezed, you can write immutable data classes effortlessly while enjoying built‑in JSON (de)serialization, pattern matching, and sealed unions.

Key Features:

  • 🔒 Immutable Data Classes

    • Guaranteed immutability

    • Built‑in copyWith

    • Automatic ==, hashCode, and toString

  • 🔄 JSON Serialization

    • Seamless encoding/decoding

    • Custom serialization hooks

    • Nested object support

  • 🎯 Union Types / Sealed Classes

    • Type‑safe pattern matching

    • Exhaustive switch handling

    • Discriminated unions

  • 🛠️ Generated Utilities

    • Deep copy

    • Equality checks

    • toString methods

    • Builder patterns

  • Zero Boilerplate

    • Minimal handwritten code

    • Maximum type safety

When to Use Freezed?
Ideal for projects requiring complex data models, API integration, state/event handling, error unions, or clean‑architecture layers.

2. Setting Up Dependencies

Add 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.5 json_serializable: ^6.7.1

Run: flutter pub get

3. Creating Data Classes

Here’s a realistic e‑commerce example in product_model.dart:

import 'package:freezed_annotation/freezed_annotation.dart';

part 'product_model.freezed.dart';

part 'product_model.g.dart';

@freezed

class ProductResponse with _$ProductResponse {

const factory ProductResponse({

required String status,

required List<Product> products,

required Metadata metadata,

}) = _ProductResponse;

factory ProductResponse.fromJson(Map<String, dynamic> json) =>

_$ProductResponseFromJson(json);

}

@freezed

class Product with _$Product {

const factory Product({

required String id,

required String name,

required double price,

required String description,

required List<String> categories,

required ProductInventory inventory,

@JsonKey(name: 'image_urls') required List<String> imageUrls,

@Default(false) bool isFeatured,

DateTime? lastUpdated,

}) = _Product;

factory Product.fromJson(Map<String, dynamic> json) =>

_$ProductFromJson(json);

}

@freezed

class ProductInventory with _$ProductInventory {

const factory ProductInventory({

required int quantity,

required String warehouse,

@Default('IN_STOCK') String status,

}) = _ProductInventory;

factory ProductInventory.fromJson(Map<String, dynamic> json) =>

_$ProductInventoryFromJson(json);

}

@freezed

class Metadata with _$Metadata {

const factory Metadata({

required int totalCount,

required int page,

@JsonKey(name: 'items_per_page') required int itemsPerPage,

}) = _Metadata;

factory Metadata.fromJson(Map<String, dynamic> json) =>

_$MetadataFromJson(json);

}

4. Code Generation

Run build_runner to generate the .freezed.dart and .g.dart files:

# Generate once

flutter pub run build_runner build --delete-conflicting-outputs

# Or watch for changes

flutter pub run build_runner watch

5. Advanced Features

5.1 Using the Generated Classes

void main() {

final jsonString = '''

{

"status": "success",

"products": [

{

"id": "prod_123",

"name": "Premium Headphones",

"price": 199.99,

"description": "High-quality wireless headphones",

"categories": ["electronics", "audio"],

"inventory": {

"quantity": 50,

"warehouse": "CENTRAL",

"status": "IN_STOCK"

},

"image_urls": [

"https://example.com/headphones1.jpg",

"https://example.com/headphones2.jpg"

],

"isFeatured": true,

"lastUpdated": "2024-03-15T14:30:00.000Z"

}

],

"metadata": {

"totalCount": 100,

"page": 1,

"items_per_page": 20

}

}

''';

final responseMap = jsonDecode(jsonString);

final productResponse = ProductResponse.fromJson(responseMap);

final firstProduct = productResponse.products.first;

print('Product name: ${firstProduct.name}');

print('Price: \$${firstProduct.price}');

final updatedProduct = firstProduct.copyWith(

price: 179.99,

inventory: firstProduct.inventory.copyWith(quantity: 45),

);

final jsonOutput = jsonEncode(productResponse.toJson());

print(jsonOutput);

}

5.2 Custom Serialization

@freezed

class Product with _$Product {

const factory Product({

required String id,

@JsonKey(name: 'product_name') required String name,

@JsonKey(fromJson: priceFromJson, toJson: priceToJson) required double price,

}) = _Product;

static double _priceFromJson(dynamic json) => (json as num).toDouble();

static num _priceToJson(double price) => price;

factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);

}

6. Best Practices

  • Naming: Clear class names; suffix models with Model if helpful.

  • Organization:

    lib/

    └── models/

    └── product/

    ├── product_model.dart

    ├── product_model.freezed.dart

    └── product_model.g.dart

Defaults: Use @Default() for standard values. Docs: Document complex fields and provide examples. 7. Troubleshooting Build Runner Conflicts:

flutter pub run build_runner clean

flutter pub run build_runner build --delete-conflicting-outputs

  • JSON Errors: Ensure nested classes have fromJson/toJson.

  • Type Mismatches: Use custom fromJson converters:

@JsonKey(fromJson: _parseDate)

final DateTime date;

static DateTime _parseDate(dynamic json) {

if (json is String) return DateTime.parse(json);

throw FormatException('Invalid date format');

}

  • Missing Part Files: Verify part 'xyz.freezed.dart'; and part 'xyz.g.dart';.

By following this guide, you’ll harness the full power of Freezed to keep your Flutter codebase clean, type‑safe, and free of boilerplate. Happy coding!