Irbisa · Cheatsheet

State with BLoC

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

Middle Developer · 22 questions

  1. 01

    What is the BLoC pattern, and what is the difference between Bloc and Cubit?

    BLoC moves business logic out of widgets: the UI sends input in, the bloc emits immutable states out, and widgets rebuild from the latest state.

    // Cubit: input is a method callclass CounterCubit extends Cubit<int> {  CounterCubit() : super(0);   void increment() => emit(state + 1);}…
  2. 02

    What do BlocBuilder, BlocListener, BlocConsumer and BlocSelector do, and what are buildWhen and listenWhen for?

    All four subscribe to the same bloc: BlocBuilder and BlocSelector rebuild UI, BlocListener runs side effects without rebuilding, and BlocConsumer does both.

    // rebuilds only when the item count changesBlocBuilder<CartBloc, CartState>(  buildWhen: (prev, curr) => prev.items.length != curr.items.length,  builder: (context, state) => Badge(label: Text('${state.items.length}')),);…
  3. 03

    Find the bug: typing in the search field rebuilds the open-todos list, though state.todos is the same list.

    class OpenTodos extends StatelessWidget {  const OpenTodos({super.key});   @override  Widget build(BuildContext context) {    return BlocSelector<TodoCubit, TodoState, List<Todo>>(      selector: (state) => state.todos.where((t) => !t.done).toList(),      builder: (context, open) => ListView.builder(        itemCount: open.length,        itemBuilder: (context, i) => TodoTile(open[i]),      ),    );  }}

    BlocSelector rebuilds when the selected value is not == to the previous one, and this selector returns a new List from .toList() every time.

  4. 04

    What is the difference between BlocProvider(create:) and BlocProvider.value, and how do you pass an existing bloc to a pushed route?

    BlocProvider(create:) builds a bloc and owns it: the bloc is closed when that provider leaves the tree.

    // owner: builds the bloc and closes it when this subtree goes awayBlocProvider(  create: (context) => CartBloc(context.read<CartRepository>()),  child: const CartPage(),);…
  5. 05

    Why does BLoC need value equality (Equatable, freezed) for states, and what goes wrong without it?

    emit drops a new state that is == to the current one (after the first emit), and BlocSelector, buildWhen and bloc_test compare with == too.

    // before: no value equality, identity decidesfinal class ProfileLoaded extends ProfileState {  ProfileLoaded(this.user);  final User user;}// emit(ProfileLoaded(u)); emit(ProfileLoaded(u));…
  6. 06

    This Cubit adds an item, but after the first one the cart on screen stops updating. Why?

    class CartState extends Equatable {  const CartState(this.items);  final List<Item> items;   @override  List<Object?> get props => [items];} class CartCubit extends Cubit<CartState> {  CartCubit() : super(CartState([]));   void add(Item item) {    state.items.add(item);    emit(state);  }}

    emit skips a state that is == to the current one.

  7. 07

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

    BlocProvider is lazy by default: create does not run until something first reads the bloc, so a bloc that subscribes in its constructor does not exist until Settings reads it.

    // right: starts at once, for a bloc whose job begins before any widget asksBlocProvider(  lazy: false,  create: (c) => SessionBloc(c.read<AuthRepository>()),  child: const App(),);…
  8. 08

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

    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.

    // wrong: the effect is a plain field, so every later state still carries itclass 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 until someone clears it…
  9. 09

    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?

    add only puts the event on the bloc's event stream.

  10. 10

    In a debug build this handler fails with "emit was called after an event handler completed normally". What did it do wrong?

    on<ProfileRequested>((event, emit) {  emit(const ProfileLoading());  _repo.fetch(event.id).then((profile) {    emit(ProfileLoaded(profile));  });});

    The handler returned before its work finished.

  11. 11

    What happens when LoggedIn is added to this bloc, and why is the catch-all handler a bug?

    class AuthBloc extends Bloc<AuthEvent, AuthState> {  AuthBloc(this._repo, this._analytics) : super(const AuthInitial()) {    // "a spinner and a log line for every auth action"    on<AuthEvent>((event, emit) async {      emit(const AuthLoading());      await _analytics.log('$event');    });    on<LoggedIn>((event, emit) async {      emit(Authenticated(await _repo.signIn(event.credentials)));    });    on<LoggedOut>((event, emit) async {      await _repo.signOut();      emit(const Unauthenticated());    });  }}

    Both handlers run: LoggedIn emits AuthLoading from the catch-all and then Authenticated from the specific one.

  12. 12

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

    Expected failures become a state; unexpected ones become telemetry.

    class FeedBloc extends Bloc<FeedEvent, FeedState> {  FeedBloc(this._api) : super(const FeedState.idle()) {    on<FeedRequested>(_onRequested);  }   final FeedApi _api;…
  13. 13

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

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

    // the emitter owns the subscriptionclass OrdersBloc extends Bloc<OrdersEvent, OrdersState> {  OrdersBloc(this._repo) : super(const OrdersLoading()) {    on<OrdersWatchStarted>(      (event, emit) => emit.forEach<List<Order>>(        _repo.watch(),…
  14. 14

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

    BlocObserver is one global listener for every Bloc and Cubit in the app.

    class AppBlocObserver extends BlocObserver {  const AppBlocObserver(this._crash);  final CrashReporter _crash;   @override  void onTransition(Bloc bloc, Transition transition) {…
  15. 15

    How do you test a Bloc or Cubit with bloc_test and a mocked repository?

    bloc_test gives you a blocTest helper that builds a bloc, fires events at it and asserts the exact sequence of states it emits, with no widget tree involved.

    class MockAuthRepository extends Mock implements AuthRepository {} void main() {  late MockAuthRepository repo;  setUp(() => repo = MockAuthRepository());…
  16. 16

    How do you persist bloc state across app restarts with hydrated_bloc?

    hydrated_bloc saves a bloc's state to local storage as JSON and restores it on the next launch, so a theme, a language or a draft survives a restart.

    Future<void> main() async {  WidgetsFlutterBinding.ensureInitialized();  HydratedBloc.storage = await HydratedStorage.build(    storageDirectory: kIsWeb        ? HydratedStorageDirectory.web        : HydratedStorageDirectory((await getApplicationSupportDirectory()).path),…
  17. 17

    How does BLoC compare with Riverpod and Provider with ChangeNotifier, and which would you pick for a chat app?

    BLoC is the most explicit and traceable but needs the most classes, Riverpod is flexible with less ceremony, ChangeNotifier is the simplest and weakest at scale.

  18. 18

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

    A transformer decides how events queue up for one on<Event> handler.

    import 'package:bloc_concurrency/bloc_concurrency.dart';import 'package:rxdart/rxdart.dart'; // debounce first, then switchMap: a new query cancels the running oneEventTransformer<E> debounceRestartable<E>(Duration d) =>    (events, mapper) => events.debounceTime(d).switchMap(mapper);…
  19. 19

    Find the bug: tapping "Add" on two products in quick succession sometimes leaves only one of them in the cart.

    class CartCubit extends Cubit<CartState> {  CartCubit(this._api) : super(const CartState());  final CartApi _api;   Future<void> add(Product product) async {    final current = state;    final saved = await _api.addToCart(product.id);    emit(current.copyWith(items: [...current.items, saved]));  }}

    current is a snapshot of state taken before the await.

  20. 20

    How do you communicate between Blocs without coupling them?

    Never let one bloc call another directly; that makes each untestable without the other.

    // screen level: the widget coordinates, neither bloc knows the otherBlocListener<AuthBloc, AuthState>(  listenWhen: (prev, curr) => prev is Authenticated && curr is Anonymous,  listener: (context, _) => context.read<CartBloc>().add(const CartCleared()),  child: const HomePage(),);…
  21. 21

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

    Keep the data and the status in one state object, so a refresh can flip the status without throwing the items away.

    enum FeedStatus { initial, loading, ready, failure } class FeedState extends Equatable {  const FeedState({this.status = FeedStatus.initial, this.posts = const [], this.error});   final FeedStatus status;…
  22. 22

    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?

    Blocs provided above MaterialApp live for the whole app run.

    MaterialApp(  // above the Navigator, so pushed routes see the providers  builder: (context, child) => BlocBuilder<AuthBloc, AuthState>(    buildWhen: (p, c) => p.userId != c.userId,    builder: (context, auth) => MultiBlocProvider(      // new key per user: old providers unmount and close their blocs…