State — BLoC
Irbisa · cheatsheetSeptember 13, 2026

State — BLoC

BLoC pattern, Cubit, Events, States, BlocBuilder, BlocConsumer

Middle Developer20 itemscompressed for a skim
  1. 01

    Explain BLoC pattern: Events, States, Bloc vs Cubit.

    Medium

    BLoC (Business Logic Component) separates UI from business logic using events and states.

    // ── CUBIT ─────────────────────────────────────────
    class CounterCubit extends Cubit<int> {
      CounterCubit() : super(0);
      void increment() => emit(state + 1);
      void decrement() => emit(state - 1);
      void reset() => emit(0);
    …
  2. 02

    What is BlocObserver and how do you use it for logging and analytics?

    Hard

    BlocObserver is a global listener for all Bloc/Cubit events, state changes, transitions, and errors.

    // ── Custom BlocObserver ─────────────────────────────
    class AppBlocObserver extends BlocObserver {
      final AnalyticsService _analytics;
      final CrashService _crash;
    
      AppBlocObserver(this._analytics, this._crash);
    …
  3. 03

    What are event transformers in BLoC and what do droppable / restartable / sequential / concurrent do?

    Hard

    Event transformers control HOW events flowing into an on<Event> handler are processed.

    import 'package:bloc_concurrency/bloc_concurrency.dart';
    import 'package:rxdart/rxdart.dart';
    
    sealed class SearchEvent {}
    final class QueryChanged extends SearchEvent { final String q; QueryChanged(this.q); }
    …
  4. 04

    What's the difference between BlocBuilder, BlocSelector, and BlocConsumer (plus buildWhen)?

    Medium

    All three subscribe to a Bloc/Cubit; they differ in what triggers a rebuild and whether they can run side effects.

    // ── BlocBuilder — render based on full state ──
    BlocBuilder<AuthBloc, AuthState>(
      builder: (context, state) {
        return switch (state) {
          AuthLoading()        => const _Spinner(),
          AuthSuccess(:final user) => HomePage(user: user),
    …
  5. 05

    How do BlocProvider, MultiBlocProvider, and BlocProvider.value differ?

    Medium

    Three ways to inject blocs into the widget tree, each with a specific role.

    // ── Single provider per screen ──
    GoRoute(
      path: '/cart',
      builder: (_, __) => BlocProvider(
        create: (ctx) => CartBloc(ctx.read<CartRepository>())..add(const Loaded()),
        child: const CartPage(),
    …
  6. 06

    How do you persist BLoC state across restarts with hydrated_bloc?

    Medium

    Out of the box, blocs lose state when the app restarts.

    // pubspec: hydrated_bloc: ^11.0.0, path_provider: ^2.1.0
    
    Future<void> main() async {
      WidgetsFlutterBinding.ensureInitialized();
      HydratedBloc.storage = await HydratedStorage.build(
        storageDirectory: kIsWeb
    …
  7. 07

    How do you test a Bloc with bloc_test?

    Medium

    bloc_test ships a blocTest helper that scripts a bloc through events and asserts the resulting state stream.

    import 'package:bloc_test/bloc_test.dart';
    import 'package:mocktail/mocktail.dart';
    import 'package:flutter_test/flutter_test.dart';
    
    class MockAuthRepo extends Mock implements AuthRepository {}
    …
  8. 08

    How do you communicate between Blocs without coupling them?

    Hard

    Blocs should not call each other directly — that couples them so tightly you cannot test either one alone.

    // ── 1) Widget-mediated — auth changes reset cart ──
    class AppShell extends StatelessWidget {
      const AppShell({super.key});
      @override Widget build(BuildContext context) {
        return BlocListener<AuthBloc, AuthState>(
          listenWhen: (prev, next) => prev is Authenticated && next is Anonymous,
    …
  9. 09

    How do you handle errors in a Bloc — try/catch, addError, and onError?

    Medium

    Error handling in BLoC has three layers, each with its own purpose.

    // ── 1) try/catch — expected failures translate to state ──
    class FeedBloc extends Bloc<FeedEvent, FeedState> {
      FeedBloc(this.api) : super(const FeedState.idle()) {
        on<FeedRequested>(_onRequested);
      }
      final FeedApi api;
    …
  10. 10

    You call emit() and nothing happens on screen. How do you find out why?

    Medium

    Nine times out of ten the bloc never emitted at all, because the new state compared equal to the current one and bloc silently drops that.

    class CartState extends Equatable {
      const CartState({this.items = const []});
      final List<Item> items;
    
      CartState copyWith({List<Item>? items}) => CartState(items: items ?? this.items);
    …
  11. 11

    Your repository exposes a Stream of updates. How do you feed it into a Bloc?

    Medium

    The subscription belongs inside the bloc, either owned by the emitter inside a handler or owned by the bloc itself and turned into private events.

    // ── Option 1: the emitter owns the subscription ──
    class OrdersBloc extends Bloc<OrdersEvent, OrdersState> {
      OrdersBloc(this._repo) : super(const OrdersLoading()) {
        on<OrdersWatchStarted>(_onWatch, transformer: restartable());
      }
    …
  12. 12

    Pull-to-refresh blanks the list because the bloc emits Loading. How would you model the state instead?

    Hard

    Keep the data and the status in the same state object so a refresh can flip the status without throwing away the items already on screen.

    enum FeedStatus { initial, loading, ready, failure }
    
    class FeedState extends Equatable {
      const FeedState({
        this.status = FeedStatus.initial,
        this.posts = const [],
    …
  13. 13

    Your SessionBloc subscribes to the auth stream in its constructor, but nothing reacts until the user opens Settings — why?

    Medium

    BlocProvider is lazy by default, so create does not run until something first reads the bloc — and a bloc whose constructor does the subscribing simply does not exist until then.

    class SessionBloc extends Bloc<SessionEvent, SessionState> {
      SessionBloc(this._auth) : super(const SessionUnknown()) {
        on<_UserChanged>((e, emit) => emit(
              e.user == null ? const SessionSignedOut() : SessionActive(e.user!),
            ));
        // this line only ever runs if something creates the bloc
    …
  14. 14

    Your "Saved" snackbar shows up a second time when the user merely toggles a filter — what is wrong with that state?

    Medium

    The one-shot effect is stored as an ordinary field in the state, so every later state still carries it and BlocListener delivers it again.

    // ❌ the effect is a plain field, so it survives into every later state
    class FeedState extends Equatable {
      const FeedState({this.posts = const [], this.filter = Filter.all, this.message});
      final List<Post> posts;
      final Filter filter;
      final String? message;          // 'Saved' stays here forever
    …
  15. 15

    A teammate added a catch-all on<AuthEvent> for logging and now every sign-in runs twice — how does bloc match events to handlers?

    Medium

    Every registered handler whose type matches the event runs; there is no first-match-wins, so a base-type handler runs alongside the specific one.

    sealed class AuthEvent {}
    final class LoggedIn extends AuthEvent {
      const LoggedIn(this.user);
      final User user;
    }
    final class LoggedOut extends AuthEvent {}
    …
  16. 16

    Pushing the details route throws "BlocProvider.of() called with a context that does not contain a Bloc" although the list screen provides it — why?

    Medium

    The pushed page is mounted under the Navigator, not under the widget that pushed it, and a provider lookup only walks its own ancestors in the element tree.

    // ❌ this context is the route's, so the lookup starts below the Navigator
    onTap: () => Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => BlocProvider.value(
          value: context.read<CartBloc>(),      // throws: no CartBloc above here
          child: const DetailsPage(),
    …
  17. 17

    Your crash logs are full of "emit was called after an event handler completed normally" — what did that handler actually do wrong?

    Hard

    It returned before its work finished: an Emitter is only valid until the future the handler returns completes, and something emitted after that moment.

    // ❌ the handler returns before the future does
    on<ProfileRequested>((event, emit) {
      _repo.fetch(event.id).then((p) => emit(ProfileLoaded(p)));   // emits into a dead emitter
    });
    
    // ❌ the subscription outlives the handler — same bug, harder to spot
    …
  18. 18

    You call bloc.add(SaveTapped()) and read bloc.state on the very next line to decide whether to pop — why is it always the previous state?

    Hard

    add only puts the event on the bloc's event stream; the handler runs a microtask later at the earliest, so at the next statement nothing has emitted yet.

    // ❌ the handler has not even started when the next line runs
    void _onSavePressed(BuildContext context) {
      context.read<FormBloc>().add(const SaveTapped());
      if (context.read<FormBloc>().state.status == Status.success) {
        Navigator.of(context).pop();          // never true here
      }
    …
  19. 19

    After signing out and signing in as another account the profile screen still shows the first user's data — what is wrong with app-scoped blocs here?

    Hard

    Blocs provided above MaterialApp live for the whole app run, so signing out only changed the route while every bloc kept the previous user's state, subscriptions and caches.

    class App extends StatelessWidget {
      const App({super.key});
    
      @override
      Widget build(BuildContext context) {
        return BlocBuilder<AuthBloc, AuthState>(
    …
  20. 20

    A like flips instantly, the API call fails, and the rollback also wipes the changes the user made meanwhile — how should optimistic updates be modelled?

    Hard

    Roll back the one entity you touched by applying the inverse edit to the current state, never by re-emitting a snapshot taken before the request.

    // ❌ snapshot rollback: undoes everything that happened during the request
    Future<void> _onLiked(LikePressed e, Emitter<FeedState> emit) async {
      final previous = state;                       // the whole list, frozen
      emit(state.copyWith(posts: _patch(state.posts, e.id, (p) => p.copyWith(liked: true))));
      try {
        await _repo.like(e.id);
    …