BLoC Design Principles

1. Single Responsibility

Each BLoC should handle a single feature or logical component of your application.

Good Example:

class AuthenticationBloc extends Bloc<AuthEvent, AuthState> { ... }
class UserProfileBloc extends Bloc<ProfileEvent, ProfileState> { ... }

Bad Example:

class UserBloc extends Bloc<UserEvent, UserState> {
  // Handling authentication, profile, settings, and preferences
  // Too many responsibilities!
}

2. Predictable State Changes

Always ensure state transitions are predictable and traceable.

Good Example:

on<FetchUserData>((event, emit) async {
  emit(state.copyWith(status: Status.loading));
  try {
    final userData = await _repository.fetchUser(event.userId);
    emit(state.copyWith(
      status: Status.success,
      user: userData,
    ));
  } catch (e) {
    emit(state.copyWith(
      status: Status.error,
      error: e.toString(),
    ));
  }
});

Bad Example:

on<FetchUserData>((event, emit) async {
  // No loading state
  // Direct state mutation
  state.user = await _repository.fetchUser(event.userId);
  emit(state);
});

Project Structure

lib/
├── blocs/
│   ├── authentication/
│   │   ├── authentication_bloc.dart
│   │   ├── authentication_event.dart
│   │   └── authentication_state.dart
│   └── user_profile/
│       ├── user_profile_bloc.dart
│       ├── user_profile_event.dart
│       └── user_profile_state.dart
├── repositories/
├── models/
└── ui/

Error Handling

1. Granular Error States

Use specific error states for different types of failures:


class AuthState with _$AuthState {
  const factory AuthState.initial() = _Initial;
  const factory AuthState.loading() = _Loading;
  const factory AuthState.success(User user) = _Success;
  const factory AuthState.networkError(String message) = _NetworkError;
  const factory AuthState.validationError(String field, String message) = _ValidationError;
  const factory AuthState.serverError(String code, String message) = _ServerError;
}

2. Error Recovery

Always provide a way to recover from errors:

class AuthenticationBloc extends Bloc<AuthEvent, AuthState> {
  AuthenticationBloc() : super(const AuthState.initial()) {
    on<AuthEvent>((event, emit) async {
      await event.when(
        login: (credentials) async {
          try {
            // Login logic
          } catch (e) {
            emit(AuthState.error(e.toString()));
            // Add retry functionality
            await Future.delayed(const Duration(seconds: 5));
            emit(const AuthState.initial());
          }
        },
        // Other events
      );
    });
  }
}

Performance Optimization

1. Debouncing Events

Use debounce for frequent events like search:

class SearchBloc extends Bloc<SearchEvent, SearchState> {
  Timer? _debounce;

  SearchBloc() : super(SearchState.initial()) {
    on<SearchTextChanged>((event, emit) async {
      if (_debounce?.isActive ?? false) _debounce?.cancel();
      _debounce = Timer(const Duration(milliseconds: 500), () async {
        emit(SearchState.loading());
        try {
          final results = await _repository.search(event.query);
          emit(SearchState.success(results));
        } catch (e) {
          emit(SearchState.error(e.toString()));
        }
      });
    });
  }

  
  Future<void> close() {
    _debounce?.cancel();
    return super.close();
  }
}

2. Distinct States

Avoid emitting duplicate states:

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<IncrementPressed>((event, emit) {
      // Only emit if the new state is different
      if (state.count != event.incrementBy) {
        emit(CounterState(state.count + event.incrementBy));
      }
    });
  }
}

Testing

1. Event Testing

blocTest<AuthBloc, AuthState>(
  'emits [loading, success] when login is successful',
  build: () => AuthBloc(mockAuthRepository),
  act: (bloc) => bloc.add(const AuthEvent.login(
    email: 'test@example.com',
    password: 'password',
  )),
  expect: () => [
    const AuthState.loading(),
    const AuthState.success(User(...)),
  ],
);

2. State Testing

test('AuthState copyWith works correctly', () {
  const initialState = AuthState.success(
    user: User(id: '1', name: 'John'),
  );
  
  final newState = initialState.copyWith(
    user: User(id: '1', name: 'John Doe'),
  );
  
  expect(newState.user.name, equals('John Doe'));
  expect(newState.user.id, equals('1'));
});

Dependency Injection

Use dependency injection for better testability:

class AuthenticationBloc extends Bloc<AuthEvent, AuthState> {
  final AuthRepository _authRepository;
  final AnalyticsService _analytics;

  AuthenticationBloc({
    required AuthRepository authRepository,
    required AnalyticsService analytics,
  })  : _authRepository = authRepository,
        _analytics = analytics,
        super(const AuthState.initial());
}

Common Pitfalls to Avoid

  1. Don’t Mix UI Logic with Business Logic

    • Keep UI-specific logic in the widgets
    • BLoC should only handle business logic
  2. Avoid State Leaks

    • Always close streams and cancel subscriptions
    • Use close() method to clean up resources
  3. Don’t Emit States Outside Event Handlers

    • All state changes should be triggered by events
    • Use events for side effects
  4. Avoid Complex State Objects

    • Keep state classes simple and focused
    • Use composition instead of inheritance for complex states
  5. Don’t Share BLoC Instances Unnecessarily

    • Use scoped providers when needed
    • Create new BLoC instances for independent features

Debugging Tips

  1. Use BlocObserver
class MyBlocObserver extends BlocObserver {
  
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    print('${bloc.runtimeType} $change');
  }

  
  void onError(BlocBase bloc, Object error, StackTrace stackTrace) {
    print('${bloc.runtimeType} $error $stackTrace');
    super.onError(bloc, error, stackTrace);
  }
}

void main() {
  Bloc.observer = MyBlocObserver();
  runApp(MyApp());
}
  1. Add Meaningful toString Methods

String toString() => 'AuthState.${this.runtimeType}';

Remember that these best practices are guidelines, not strict rules. Adapt them to your specific use case while maintaining clean architecture principles.