Priyam Soni

Nov 04, 2024 • 3 min read

Guide to understand and use Freezed package in Flutter

Code Generation for Immutable Classes in Flutter/Dart

Guide to understand and use Freezed package in Flutter

1. Introduction

What is Freezed?

A powerful code generation package that removes the boilerplate from your Dart/Flutter models. It makes writing immutable data classes a breeze while providing extensive features for JSON serialization, pattern matching, and sealed unions.

Key Features

  • 🔒 Immutable Data Classes

    • Guaranteed immutability for your models

    • Built-in copyWith functionality

    • Automatic ==, hashCode, and toString implementations

  • 🔄 JSON Serialization

    • Seamless JSON encoding/decoding

    • Custom serialization support

    • Nested object handling

  • 🎯 Union Types / Sealed Classes

    • Type-safe pattern matching

    • Exhaustive switch statements

    • Discriminated unions

  • 🛠️ Generated Utilities

    • Deep copy functionality

    • Equality comparison

    • toString methods

    • Builder patterns

  • Zero Boilerplate

    • Minimal code writing

    • Maximum type safety

    • Clean, maintainable codebase

When to Use Freezed?

Perfect for projects that need:

  • Complex data models

  • API integration

  • State management

  • Event handling

  • Error handling with union types

  • Clean architecture implementation

2. Setting Up Dependencies

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

Run:

flutter pub get

3. Creating Data Classes

Let's create a more realistic example with an e-commerce product model:

// 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 the build_runner to generate the necessary code:

# One-time generation
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
    }
  }
  ''';

  // Parsing JSON
  final Map<String, dynamic> responseMap = jsonDecode(jsonString);
  final productResponse = ProductResponse.fromJson(responseMap);

  // Accessing data
  final firstProduct = productResponse.products.first;
  print('Product name: ${firstProduct.name}');
  print('Price: \$${firstProduct.price}');

  // Using copyWith
  final updatedProduct = firstProduct.copyWith(
    price: 179.99,
    inventory: firstProduct.inventory.copyWith(quantity: 45),
  );

  // Converting back to JSON
  final jsonOutput = jsonEncode(productResponse.toJson());
  print(jsonOutput);
}

5.2 Custom Serialization

You can customize JSON serialization using @JsonKey:

Common scenarios where @JsonKey is essential:

  1. Backend API returns different field names than your Dart model

  2. Need to transform data during serialization/deserialization

  3. Handle nullable or missing fields with default values

  4. Custom parsing for complex data types

  5. Field value validation

  6. Conditional 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

  1. Naming Conventions:

    • Use clear, descriptive names for your classes

    • End model class names with "Model" if they represent data models

    • Use meaningful names for union cases

  2. File Organization:

    lib/
    ├── models/
    │   ├── product/
    │   │   ├── product_model.dart
    │   │   ├── product_model.freezed.dart
    │   │   └── product_model.g.dart
    │   └── ...
  3. Default Values:

    • Use @Default() for fields that should have default values

    • Consider making non-critical fields nullable

  4. Documentation:

    • Add documentation comments to your classes and complex fields

    • Include examples in the documentation

7. Troubleshooting

Common issues and solutions:

  1. Build Runner Issues:

    # Clean and rebuild
    flutter pub run build_runner clean
    flutter pub run build_runner build --delete-conflicting-outputs
  2. JSON Serialization Errors:

    • Ensure all nested objects have proper fromJson/toJson implementations

    • Check that required fields are present in JSON

    • Verify data types match between JSON and model

  3. Type Errors:

    // Handle potential type mismatches
    @JsonKey(fromJson: _parseDate)
    final DateTime date;
    
    static DateTime _parseDate(dynamic json) {
      if (json is String) {
        return DateTime.parse(json);
      }
      throw FormatException('Invalid date format');
    }
  4. Missing Part Files:

    • Ensure you have both part statements:

      part 'model_name.freezed.dart'; part 'model_name.g.dart';
    • Run build_runner after adding new files

Remember to regularly update your dependencies to get the latest features and bug fixes.

Join Priyam on Peerlist!

Join amazing folks like Priyam and thousands of other people in tech.

Create Profile

Join with Priyam’s personal invite link.

0

6

0