Why Freezed with BLoC?

Freezed is a code generation package that helps create immutable classes in Dart. When combined with BLoC, it provides several advantages:

  1. Immutability by Default

    • Ensures state objects can’t be modified after creation
    • Prevents accidental state mutations
    • Makes state management more predictable
  2. Reduced Boilerplate

    • Automatically generates equality comparisons
    • Creates copyWith methods
    • Handles serialization/deserialization
  3. Union Types / Sealed Classes

    • Perfect for representing different states
    • Type-safe pattern matching
    • Compile-time safety

Setting Up Freezed

First, add the required 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

Creating a BLoC with Freezed

Let’s create a simple authentication BLoC using Freezed:

1. Define Events

import 'package:freezed_annotation/freezed_annotation.dart';

part 'auth_event.freezed.dart';


class AuthEvent with _$AuthEvent {
  const factory AuthEvent.login({
    required String email,
    required String password,
  }) = _Login;
  
  const factory AuthEvent.logout() = _Logout;
  
  const factory AuthEvent.signUp({
    required String email,
    required String password,
    required String name,
  }) = _SignUp;
}

2. Define States

import 'package:freezed_annotation/freezed_annotation.dart';

part 'auth_state.freezed.dart';


class AuthState with _$AuthState {
  const factory AuthState.initial() = _Initial;
  
  const factory AuthState.loading() = _Loading;
  
  const factory AuthState.authenticated({
    required String userId,
    required String email,
  }) = _Authenticated;
  
  const factory AuthState.error({
    required String message,
  }) = _Error;
}

3. Implement the BLoC

class AuthBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository _authRepository;

  AuthBloc(this._authRepository) : super(const AuthState.initial()) {
    on<AuthEvent>((event, emit) async {
      await event.when(
        login: (email, password) async {
          emit(const AuthState.loading());
          try {
            final user = await _authRepository.login(email, password);
            emit(AuthState.authenticated(
              userId: user.id,
              email: user.email,
            ));
          } catch (e) {
            emit(AuthState.error(message: e.toString()));
          }
        },
        logout: () async {
          emit(const AuthState.loading());
          try {
            await _authRepository.logout();
            emit(const AuthState.initial());
          } catch (e) {
            emit(AuthState.error(message: e.toString()));
          }
        },
        signUp: (email, password, name) async {
          // Similar implementation to login
        },
      );
    });
  }
}

Using the Freezed BLoC in UI

class LoginScreen extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return BlocBuilder<AuthBloc, AuthState>(
      builder: (context, state) {
        return state.when(
          initial: () => LoginForm(),
          loading: () => LoadingIndicator(),
          authenticated: (userId, email) => DashboardScreen(),
          error: (message) => ErrorWidget(message),
        );
      },
    );
  }
}

Benefits of This Approach

  1. Type Safety

    // The compiler ensures we handle all possible states
    state.when(
      initial: () => /* must be handled */,
      loading: () => /* must be handled */,
      authenticated: (userId, email) => /* must be handled */,
      error: (message) => /* must be handled */,
    );
    
  2. Immutable State

    // Creating new states is type-safe and immutable
    AuthState.authenticated(
      userId: 'user123',
      email: 'user@example.com',
    );
    
  3. Easy Copying

    // Automatically generated copyWith method
    final newState = oldState.copyWith(email: 'new@email.com');
    

Generating Code

After defining your Freezed classes, run the build_runner to generate the necessary code:

flutter pub run build_runner build

Or for continuous generation during development:

flutter pub run build_runner watch

Best Practices with Freezed and BLoC

  1. Keep States Minimal

    • Only include necessary data in state classes
    • Use separate models for complex data structures
  2. Use Union Types Effectively

    • Create meaningful state variations
    • Avoid boolean flags in state classes
  3. Leverage Pattern Matching

    • Use when for exhaustive state handling
    • Use maybeWhen when you only care about specific states
  4. Handle Loading States

    • Consider using data-specific loading states
    • Combine loading with previous data when appropriate
  5. Error Handling

    • Create specific error states for different scenarios
    • Include relevant error information in the state