🧱 Flutter Bloc Cheat Sheet

BLoC pattern with clean architecture — events, states, Cubit, builders.

🔍
🟢 Cubit (Simple State)
Basic Cubit
import 'package:flutter_bloc/flutter_bloc.dart';

// State
class CounterState {
  final int count;
  const CounterState(this.count);
}

// Cubit
class CounterCubit extends Cubit<CounterState> {
  CounterCubit() : super(const CounterState(0));

  void increment() => emit(CounterState(state.count + 1));
  void decrement() => emit(CounterState(state.count - 1));
  void reset()     => emit(const CounterState(0));
}
Async Cubit with sealed state
// States (sealed class — Dart 3)
sealed class UserState {}
class UserInitial  extends UserState {}
class UserLoading  extends UserState {}
class UserLoaded   extends UserState {
  final User user;
  UserLoaded(this.user);
}
class UserError extends UserState {
  final String message;
  UserError(this.message);
}

// Cubit
class UserCubit extends Cubit<UserState> {
  final UserRepository _repo;

  UserCubit(this._repo) : super(UserInitial());

  Future<void> loadUser(String id) async {
    emit(UserLoading());
    try {
      final user = await _repo.getUser(id);
      emit(UserLoaded(user));
    } catch (e) {
      emit(UserError(e.toString()));
    }
  }
}
🔵 BLoC (Events → States)
Events
// user_event.dart
sealed class UserEvent {}

class UserLoadRequested extends UserEvent {
  final String id;
  UserLoadRequested(this.id);
}

class UserRefreshRequested extends UserEvent {}

class UserLogoutRequested extends UserEvent {}
BLoC implementation
// user_bloc.dart
class UserBloc extends Bloc<UserEvent, UserState> {
  final GetUserUseCase _getUser;

  UserBloc({required GetUserUseCase getUserUseCase})
      : _getUser = getUserUseCase,
        super(UserInitial()) {
    on<UserLoadRequested>(_onLoadRequested);
    on<UserRefreshRequested>(_onRefreshRequested);
    on<UserLogoutRequested>(_onLogout);
  }

  Future<void> _onLoadRequested(
    UserLoadRequested event,
    Emitter<UserState> emit,
  ) async {
    emit(UserLoading());
    final result = await _getUser(event.id);
    result.fold(
      (failure) => emit(UserError(failure.message)),
      (user)    => emit(UserLoaded(user)),
    );
  }

  Future<void> _onRefreshRequested(
    UserRefreshRequested event,
    Emitter<UserState> emit,
  ) async {
    if (state is UserLoaded) {
      final current = (state as UserLoaded).user;
      // refresh with current user id
      add(UserLoadRequested(current.id));
    }
  }

  void _onLogout(UserLogoutRequested event, Emitter<UserState> emit) {
    emit(UserInitial());
  }
}
Transformers (debounce, throttle)
// Add bloc_concurrency package
import 'package:bloc_concurrency/bloc_concurrency.dart';

// Only one at a time — drop if already running
on<SearchEvent>(_onSearch, transformer: droppable());

// Last one wins — cancel previous
on<SearchEvent>(_onSearch, transformer: restartable());

// Queue all events in order
on<SearchEvent>(_onSearch, transformer: sequential());

// Debounce with RxDart
import 'package:rxdart/rxdart.dart';

on<SearchChanged>(
  _onSearchChanged,
  transformer: (events, mapper) => events
      .debounceTime(const Duration(milliseconds: 300))
      .switchMap(mapper),
);
📦 State Best Practices
Equatable state (avoid rebuilds)
// Add equatable package
class UserLoaded extends UserState with EquatableMixin {
  final User user;
  const UserLoaded(this.user);

  @override
  List<Object> get props => [user];   // only rebuild when user changes
}

// Or freeze package (code generation)
// @freezed
// class UserState with _$UserState { ... }
CopyWith pattern for complex state
class HomeState {
  final List<Post> posts;
  final bool isLoading;
  final String? error;
  final int page;

  const HomeState({
    this.posts   = const [],
    this.isLoading = false,
    this.error,
    this.page    = 1,
  });

  HomeState copyWith({
    List<Post>? posts,
    bool? isLoading,
    String? error,
    int? page,
  }) => HomeState(
    posts:     posts     ?? this.posts,
    isLoading: isLoading ?? this.isLoading,
    error:     error,            // allow null to clear error
    page:      page      ?? this.page,
  );
}

// In Cubit
emit(state.copyWith(isLoading: true, error: null));
emit(state.copyWith(posts: newPosts, isLoading: false));
🏗️ Providing Blocs
BlocProvider
// Provide to widget tree
BlocProvider(
  create: (context) => sl<UserBloc>()..add(UserLoadRequested('1')),
  child: const UserScreen(),
)

// Provide existing instance
BlocProvider.value(
  value: existingBloc,
  child: const SomeWidget(),
)

// Multiple blocs
MultiBlocProvider(
  providers: [
    BlocProvider(create: (_) => sl<UserBloc>()),
    BlocProvider(create: (_) => sl<PostBloc>()),
    BlocProvider(create: (_) => sl<ThemeCubit>()),
  ],
  child: const App(),
)
Access bloc without providing
// Read (no rebuild)
context.read<UserBloc>().add(UserRefreshRequested());

// Watch (rebuilds on state change)
final state = context.watch<UserBloc>().state;

// Select (rebuilds only on specific value change)
final name = context.select<UserBloc, String>(
  (bloc) => bloc.state is UserLoaded
    ? (bloc.state as UserLoaded).user.name
    : '',
);
🔨 BlocBuilder / Listener / Consumer
BlocBuilder
BlocBuilder<UserBloc, UserState>(
  // Optional: only rebuild for specific states
  buildWhen: (prev, curr) => curr is! UserLoading,
  builder: (context, state) => switch (state) {
    UserInitial()       => const SizedBox(),
    UserLoading()       => const CircularProgressIndicator(),
    UserLoaded(:final user)  => UserCard(user: user),
    UserError(:final message) => ErrorView(message: message),
  },
)
BlocListener (side effects)
BlocListener<UserBloc, UserState>(
  listenWhen: (prev, curr) => curr is UserError,
  listener: (context, state) {
    if (state is UserError) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text(state.message)),
      );
    }
    if (state is UserLoaded && state.user.needsOnboarding) {
      context.go('/onboarding');
    }
  },
  child: const UserContent(),
)
BlocConsumer (build + listen)
BlocConsumer<UserBloc, UserState>(
  listenWhen: (prev, curr) => curr is UserError || curr is UserLoaded,
  listener: (context, state) {
    if (state is UserError) {
      _showErrorDialog(context, state.message);
    }
  },
  buildWhen: (prev, curr) => curr is! UserError,
  builder: (context, state) {
    if (state is UserLoading) return const LoadingWidget();
    if (state is UserLoaded) return UserCard(user: state.user);
    return const SizedBox();
  },
)

// Multiple listeners
MultiBlocListener(
  listeners: [
    BlocListener<UserBloc, UserState>(listener: ...),
    BlocListener<PostBloc, PostState>(listener: ...),
  ],
  child: const Content(),
)
👁️ BlocObserver
Global BLoC observer for debugging
class AppBlocObserver extends BlocObserver {
  @override
  void onCreate(BlocBase bloc) {
    super.onCreate(bloc);
    debugPrint('onCreate: ${bloc.runtimeType}');
  }

  @override
  void onEvent(Bloc bloc, Object? event) {
    super.onEvent(bloc, event);
    debugPrint('onEvent: ${bloc.runtimeType} — $event');
  }

  @override
  void onChange(BlocBase bloc, Change change) {
    super.onChange(bloc, change);
    debugPrint('onChange: ${bloc.runtimeType}\n'
        '  current: ${change.currentState}\n'
        '  next:    ${change.nextState}');
  }

  @override
  void onError(BlocBase bloc, Object error, StackTrace st) {
    debugPrint('onError: ${bloc.runtimeType} — $error');
    super.onError(bloc, error, st);
  }
}

// Register in main
void main() {
  Bloc.observer = AppBlocObserver();
  runApp(const MyApp());
}
🏛️ Clean Architecture Structure
Folder structure
lib/
├── core/
│   ├── error/          # failures, exceptions
│   ├── usecase/        # UseCase base class
│   ├── utils/
│   └── di/             # service locator
├── features/
│   └── user/
│       ├── data/
│       │   ├── datasources/
│       │   │   ├── user_remote_ds.dart
│       │   │   └── user_local_ds.dart
│       │   ├── models/
│       │   │   └── user_model.dart
│       │   └── repositories/
│       │       └── user_repo_impl.dart
│       ├── domain/
│       │   ├── entities/
│       │   │   └── user.dart
│       │   ├── repositories/
│       │   │   └── user_repository.dart   # abstract
│       │   └── usecases/
│       │       └── get_user.dart
│       └── presentation/
│           ├── bloc/
│           │   ├── user_bloc.dart
│           │   ├── user_event.dart
│           │   └── user_state.dart
│           ├── pages/
│           │   └── user_screen.dart
│           └── widgets/
│               └── user_card.dart
└── main.dart
UseCase base class
// core/usecase/usecase.dart
import 'package:fpdart/fpdart.dart';
import '../error/failures.dart';

abstract class UseCase<Type, Params> {
  Future<Either<Failure, Type>> call(Params params);
}

class NoParams {}

// Example use case
class GetUserUseCase implements UseCase<User, String> {
  final UserRepository _repo;
  GetUserUseCase(this._repo);

  @override
  Future<Either<Failure, User>> call(String id) => _repo.getUser(id);
}
⚡ Tips & Patterns
Emit safely (check if closed)
// In a Cubit/Bloc
Future<void> loadData() async {
  if (isClosed) return;      // guard before async
  emit(Loading());
  final result = await _repo.fetch();
  if (isClosed) return;      // guard after async
  emit(Loaded(result));
}
Stream subscription in Bloc
class NotificationBloc extends Bloc<NotificationEvent, NotificationState> {
  late final StreamSubscription _sub;

  NotificationBloc(NotificationService svc) : super(NotificationInitial()) {
    _sub = svc.stream.listen((n) => add(NotificationReceived(n)));
    on<NotificationReceived>(_onReceived);
  }

  @override
  Future<void> close() {
    _sub.cancel();
    return super.close();
  }
}
Testing a Cubit / Bloc
// Add bloc_test package
import 'package:bloc_test/bloc_test.dart';

blocTest<CounterCubit, CounterState>(
  'emits [CounterState(1)] when increment is called',
  build: () => CounterCubit(),
  act: (cubit) => cubit.increment(),
  expect: () => [const CounterState(1)],
);

// Bloc test
blocTest<UserBloc, UserState>(
  'emits [loading, loaded] when UserLoadRequested added',
  build: () {
    when(() => mockRepo.getUser('1')).thenAnswer((_) async => fakeUser);
    return UserBloc(getUserUseCase: GetUserUseCase(mockRepo));
  },
  act: (bloc) => bloc.add(UserLoadRequested('1')),
  expect: () => [UserLoading(), UserLoaded(fakeUser)],
);