State — BLoC
BLoC pattern, Cubit, Events, States, BlocBuilder, BlocConsumer
- 01
Explain BLoC pattern: Events, States, Bloc vs Cubit.
MediumBLoC (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); … - 02
What is BlocObserver and how do you use it for logging and analytics?
HardBlocObserver 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); … - 03
What are event transformers in BLoC and what do droppable / restartable / sequential / concurrent do?
HardEvent 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); } … - 04
What's the difference between BlocBuilder, BlocSelector, and BlocConsumer (plus buildWhen)?
MediumAll 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), … - 05
How do BlocProvider, MultiBlocProvider, and BlocProvider.value differ?
MediumThree 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(), … - 06
How do you persist BLoC state across restarts with hydrated_bloc?
MediumOut 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 … - 07
How do you test a Bloc with bloc_test?
Mediumbloc_test ships a
blocTesthelper 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 {} … - 08
How do you communicate between Blocs without coupling them?
HardBlocs 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, … - 09
How do you handle errors in a Bloc — try/catch, addError, and onError?
MediumError 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
You call emit() and nothing happens on screen. How do you find out why?
MediumNine 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
Your repository exposes a Stream of updates. How do you feed it into a Bloc?
MediumThe 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
Pull-to-refresh blanks the list because the bloc emits Loading. How would you model the state instead?
HardKeep 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
Your SessionBloc subscribes to the auth stream in its constructor, but nothing reacts until the user opens Settings — why?
MediumBlocProvideris lazy by default, socreatedoes 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
Your "Saved" snackbar shows up a second time when the user merely toggles a filter — what is wrong with that state?
MediumThe one-shot effect is stored as an ordinary field in the state, so every later state still carries it and
BlocListenerdelivers 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
A teammate added a catch-all
on<AuthEvent>for logging and now every sign-in runs twice — how does bloc match events to handlers?MediumEvery 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
Pushing the details route throws "BlocProvider.of() called with a context that does not contain a Bloc" although the list screen provides it — why?
MediumThe 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
Your crash logs are full of "emit was called after an event handler completed normally" — what did that handler actually do wrong?
HardIt returned before its work finished: an
Emitteris 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
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?
Hardaddonly 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
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?
HardBlocs provided above
MaterialApplive 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
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?
HardRoll 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); …