Irbisa · Шпаргалка

Состояние на BLoC

Паттерн BLoC, Cubit, Events, States, BlocBuilder, BlocConsumer

Middle-разработчик · 22 вопроса

  1. 01

    Что такое паттерн BLoC и чем Bloc отличается от Cubit?

    BLoC выносит бизнес-логику из виджетов: UI передаёт ввод внутрь, блок выпускает наружу неизменяемые состояния, а виджеты перестраиваются по последнему из них.

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

    Что делают BlocBuilder, BlocListener, BlocConsumer и BlocSelector, и зачем нужны buildWhen и listenWhen?

    Все четыре подписываются на один и тот же блок: BlocBuilder и BlocSelector перестраивают UI, BlocListener выполняет побочные эффекты без перестройки, а BlocConsumer делает и то и другое.

    // 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

    Найдите баг: ввод в поле поиска перестраивает список открытых задач, хотя state.todos остаётся тем же списком.

    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 перестраивается, когда выбранное значение не == предыдущему, а этот selector каждый раз возвращает новый List из .toList().

  4. 04

    Чем BlocProvider(create:) отличается от BlocProvider.value и как передать существующий блок на новый маршрут?

    BlocProvider(create:) создаёт блок и владеет им: блок закрывается, когда этот провайдер уходит из дерева.

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

    Зачем BLoC нужно равенство по значению (Equatable, freezed) для состояний и что ломается без него?

    emit отбрасывает новое состояние, если оно == текущему (после первого emit), и BlocSelector, buildWhen и bloc_test тоже сравнивают через ==.

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

    Этот Cubit добавляет товар, но после первого товара корзина на экране перестаёт обновляться. Почему?

    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 пропускает состояние, которое == текущему.

  7. 07

    SessionBloc подписывается на поток авторизации в конструкторе, но приложение не реагирует, пока пользователь не откроет Settings: почему?

    BlocProvider по умолчанию ленивый: create не выполняется, пока блок кто-нибудь не прочитает, а блок, который подписывается в конструкторе, до этого момента просто не существует.

    // 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

    Снекбар «Сохранено» появляется второй раз, когда пользователь просто переключает фильтр: что не так с состоянием?

    Разовый эффект лежит в состоянии обычным полем, поэтому все следующие состояния тащат его за собой, и BlocListener показывает его снова.

    // 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

    Вы вызываете bloc.add(SaveTapped()) и на следующей же строке читаете bloc.state, чтобы решить, закрывать ли экран: почему там всегда старое состояние?

    add лишь кладёт событие в поток событий блока.

  10. 10

    В debug-сборке этот обработчик падает с «emit was called after an event handler completed normally». Что он сделал не так?

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

    Обработчик вернул управление раньше, чем закончил работу.

  11. 11

    Что произойдёт, когда в этот блок добавят LoggedIn, и почему общий обработчик считается багом?

    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());    });  }}

    Выполнятся оба обработчика: LoggedIn выпустит AuthLoading из общего обработчика, а затем Authenticated из конкретного.

  12. 12

    Как обрабатывать ошибки в Bloc: try/catch, addError и onError?

    Ожидаемые сбои становятся состоянием, а неожиданные уходят в телеметрию.

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

    Репозиторий отдаёт Stream обновлений. Как завести его в Bloc?

    Подписка должна жить внутри блока: либо ею владеет emitter внутри обработчика, либо сам блок, превращая данные в приватные события.

    // 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

    Что такое BlocObserver и как использовать его для логирования и отчётов о падениях?

    BlocObserver работает как один глобальный слушатель всех Bloc и Cubit в приложении.

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

    Как тестировать Bloc или Cubit через bloc_test с замоканным репозиторием?

    bloc_test даёт хелпер blocTest: он собирает блок, отправляет в него события и проверяет точную последовательность выпущенных состояний, без дерева виджетов.

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

    Как сохранить состояние блока между перезапусками приложения через hydrated_bloc?

    hydrated_bloc сохраняет состояние блока в локальное хранилище как JSON и восстанавливает при следующем запуске; так переживают перезапуск тема, язык или черновик.

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

    Как BLoC соотносится с Riverpod и Provider с ChangeNotifier, и что бы вы выбрали для чата?

    BLoC самый явный и прослеживаемый, но требует больше всего классов, Riverpod гибок и с меньшим количеством церемоний, ChangeNotifier проще всего и слабее всего на масштабе.

  18. 18

    Что такое трансформеры событий в BLoC и что делают concurrent, sequential, droppable и restartable?

    Трансформер решает, как события выстраиваются в очередь к одному обработчику on<Event>.

    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

    Найдите баг: если быстро нажать «Добавить» на двух товарах подряд, в корзине иногда оказывается только один из них.

    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 хранит снимок state, сделанный до await.

  20. 20

    Как организовать коммуникацию между Bloc-ами без жёсткой связи?

    Не давайте одному блоку звать другой напрямую: так ни один из них нельзя протестировать отдельно.

    // 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 очищает список, потому что bloc выпускает Loading. Как смоделировать состояние иначе?

    Держите данные и статус в одном объекте состояния, чтобы обновление меняло статус, не выбрасывая уже показанные элементы.

    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

    После выхода и входа под другим аккаунтом экран профиля всё ещё показывает данные первого пользователя: что не так с блоками уровня приложения?

    Блоки, объявленные выше MaterialApp, живут всё время работы приложения.

    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…