5.11. BLoC: библиотека

Ранее мы обсудили принцип работы паттерна BLoC. Вы познакомились с потоками данных, однако реализация даже простых задач требовала значительного объёма кода. Для решения этой проблемы существует более простой подход, который мы рассмотрим в этом параграфе.

Встречайте — библиотека bloc от Феликса Анжелова (англ. Felix Angelov). Это инструмент, который значительно упрощает работу с паттерном BLoC. Она избавляет от рутинных задач, минимизирует риск ошибок в потоках данных и сокращает объём шаблонного кода за счёт готовых абстракций и встроенных механизмов управления состоянием.

Мы уже упоминали вскользь об этой библиотеке и о том, как она повлияла на реализацию паттерна BLoC, в предыдущем параграфе. Теперь самое время познакомиться с библиотекой поближе и научиться её использовать.

Первые шаги

Примечание

Примечание

В этом параграфе используются следующие версии библиотек:

  • bloc: ^9.0.0

  • flutter_bloc: ^9.1.1

  • bloc_concurrency: ^0.3.0

Для начала работы подключим библиотеку к Flutter-проекту:

1flutter pub add bloc

Создаём файл counter_bloc.dart и импортируем библиотеку:

1import 'package:bloc/bloc.dart';

Класс Bloc<Event, State> — это сердце библиотеки bloc. Он принимает события (Event) и преобразует их в состояния (State), инкапсулируя всю бизнес-логику вашего приложения. Вы уже знакомы с концепцией событий и состояний, так что давайте сразу перейдём к делу: как это работает на практике.

Создаём CounterBloc, который будет реализовывать механизм счётчика. В качестве события (Bloc<CounterEvent, ...>) укажем класс CounterEvent, а в качестве состояния укажем int (Bloc<CounterEvent, int>).

1sealed class CounterEvent {
2  const CounterEvent();
3
4  // Событие для увеличения счётчика
5  const factory CounterEvent.increment() = IncrementCounterEvent;
6
7  // Событие для уменьшения счётчика
8  const factory CounterEvent.decrement() = DecrementCounterEvent;
9}
10
11final class IncrementCounterEvent extends CounterEvent {
12  const IncrementCounterEvent();
13}
14
15final class DecrementCounterEvent extends CounterEvent {
16  const DecrementCounterEvent();
17}
18
19class CounterBloc extends Bloc<CounterEvent, int> {
20  CounterBloc(super.state) {
21    ...
22  }
23  ...
24}

При ручной реализации паттерна BLoC требовалось создавать StreamController, настраивать преобразование событий через mapEventToStates, управлять подписками и обрабатывать ошибки вручную.

Библиотека bloc предлагает простое решение — метод on<Event>. Он работает как умный диспетчер: принимает события определённого типа и автоматически направляет их в нужный обработчик, избавляя вас от необходимости писать весь этот низкоуровневый код.

Примечание

Примечание

Метод on<Event> был внедрён в ответ на проблему в Dart SDK, связанную с вложенными асинхронными генераторами.

Феликс Анжелов разработал это решение, заменив использование вложенных асинхронных генераторов на более надёжный подход с методом on<Event>.

Рассмотрим структуру и применение метода on<Event>:

  • Тип события. Определяется через параметр типа <IncrementCounterEvent> и <DecrementCounterEvent>.

  • Обработка. Реализуется через функцию-обработчик.

1class CounterBloc extends Bloc<CounterEvent, int> {
2  CounterBloc(super.state) {
3    on<IncrementCounterEvent>(_increment);
4    on<DecrementCounterEvent>(_decrement);
5  }
6
7  Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) async {
8    final newState = state + 1;
9    emit(newState);
10  }
11
12  Future<void> _decrement(DecrementCounterEvent event, Emitter<int> emit) async {
13    final newState = state - 1;
14    emit(newState);
15  }
16}

Класс Emitter предоставляет нам возможность установить состояние. Можно воспринимать его как метод setState, который принимает новое состояние. У этого класса есть метод void call(State state), поэтому мы можем использовать его как функцию.

Теперь, когда мы создали наш первый Bloc, давайте рассмотрим несколько ключевых правил, которые обеспечивают его корректную и эффективную работу.

Регистрация событий

Каждое событие должно быть зарегистрировано в Bloc через соответствующий обработчик. Это обеспечивает контроль над обработкой событий и предотвращает возникновение ошибок.

Принцип работы:

  • Регистрация события через on<IncrementCounterEvent> делает его доступным для обработки.

  • Отсутствие регистрации при вызове bloc.add(const IncrementCounterEvent()) приведёт к StateError (механизм реализован через assert).

1class CounterBloc extends Bloc<CounterEvent, int> {
2  CounterBloc(super.state) {
3    // Регистрация обработчиков событий
4    on<IncrementCounterEvent>(_increment); // ✅
5    on<DecrementCounterEvent>(_decrement); // ✅
6    // Отсутствие регистрации приведёт к ошибке
7  }
8}

Управление потоком событий и bloc_concurrency

При параллельной обработке событий возможно возникновение состояния гонки. Рассмотрим пример:

1class CounterBloc extends Bloc<CounterEvent, int> {
2  CounterBloc(super.state) {
3    on<IncrementCounterEvent>(_increment);
4    ...
5  }
6
7  Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) async {
8    final newState = state + 1;
9    // Имитация асинхронной операции
10    await Future.delayed(Duration(seconds: 1));
11    emit(newState);
12  }
13
14  ...
15}

При одновременном добавлении двух событий:

1final bloc = CounterBloc(0);
2bloc.add(const CounterEvent.increment());
3bloc.add(const CounterEvent.increment());

Возникает следующая ситуация:

  • Оба обработчика получают final newState = state + 1; = 1.

  • Оба устанавливают state = 1 (вместо ожидаемой последовательности 0 -> 1 -> 2).

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

Для решения этой проблемы используется механизм трансформеров, которые предоставляет библиотека bloc_concurrency.

Добавим библиотеку bloc_concurrency:

1flutter pub add bloc_concurrency

Импортируем библиотеку:

1import 'package:bloc_concurrency/bloc_concurrency.dart';

Эта библиотека содержит реализации трансформеров для разного способа обработки событий. Устанавливаем нашему обработчику IncrementCounterEvent трансформер sequential():

1class CounterBloc extends Bloc<CounterEvent, int> {
2  CounterBloc(super.state) {
3    on<IncrementCounterEvent>(_increment, transformer: sequential());
4    ...
5  }
6
7  Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) {
8    final newState = state + 1;
9    await Future.delayed(Duration(seconds: 1));
10    emit(newState);
11  }
12
13  ...
14}

Трансформер sequential обеспечивает последовательную обработку событий:

  • Первое событие выполнится и установит значение state равным 1.
  • Второе событие выполнится после первого и установит значение state равным 2.

У каждого обработчика on<Event> своя очередь обработки. Мы уже определили, что IncrementCounterEvent обрабатывается в порядке очереди с помощью трансформера sequential. Но для DecrementCounterEvent трансформер не был указан, и у него есть собственная очередь. Даже если мы добавим трансформер sequential для обоих обработчиков, это не создаст общую очередь.

Чтобы события DecrementCounterEvent и IncrementCounterEvent имели общую очередь, нужно использовать только один обработчик on<Event>. Это позволит объединить очереди в одну. Давайте доработаем наш CounterBloc:

1class CounterBloc extends Bloc<CounterEvent, int> {
2  CounterBloc(super.state) {
3    on<CounterEvent>(
4      (event, emit) => switch (event) {
5        IncrementCounterEvent() => _increment(event, emit),
6        DecrementCounterEvent() => _decrement(event, emit),
7      },
8      transformer: sequential(),
9    );
10  }
11
12  Future<void> _increment(IncrementCounterEvent event, Emitter<int> emit) async {
13    final newState = state + 1;
14    await Future.delayed(const Duration(seconds: 1));
15    emit(newState);
16  }
17
18  Future<void> _decrement(DecrementCounterEvent event, Emitter<int> emit) async {
19    final newState = state - 1;
20    await Future.delayed(const Duration(seconds: 1));
21    emit(newState);
22  }
23}

Мы избавились от регистрации нескольких обработчиков в пользу одного обработчика, который обобщён родительским типом CounterEvent. Использование switch также гарантирует, что мы не забудем зарегистрировать и обработать событие.

Теперь все события обрабатываются в порядке очереди. Этот подход рекомендуется использовать в большинстве случаев.

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

1class FilterBloc extends Bloc<FilterEvent, FilterState> {
2  FilterBloc() : super(FilterState.initial()) {
3    on<FilterEvent>(
4      (event, emit) => switch (event) {
5        SetCategoryFilterEvent() => _setCategoryFilter(event, emit),
6        SetPriceFilterEvent() => _setPriceFilter(event, emit),
7        SetRatingFilterEvent() => _setRatingFilter(event, emit),
8      },
9      transformer: concurrent(),
10    );
11  }
12
13  void _setCategoryFilter(SetCategoryFilterEvent event, Emitter<FilterState> emit) {
14    emit(state.copyWith(categoryFilter: event.category));
15  }
16
17  void _setPriceFilter(SetPriceFilterEvent event, Emitter<FilterState> emit) {
18    emit(state.copyWith(priceFilter: event.priceRange));
19  }
20
21  void _setRatingFilter(SetRatingFilterEvent event, Emitter<FilterState> emit) {
22    emit(state.copyWith(ratingFilter: event.minRating));
23  }
24}

Параллельная обработка позволяет применять разные фильтры одновременно (категория, цена, рейтинг), каждый фильтр изменяется независимо, не блокируя другие, состояние безопасно обновляется, так как все операции синхронные.

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

1// Установка последовательной обработки по умолчанию
2Bloc.transformer = sequential();

Для возврата к параллельной обработке:

1// Установка параллельной обработки по умолчанию
2Bloc.transformer = concurrent();

Доступные стратегии

Библиотека bloc_concurrency предоставляет различные стратегии для управления событиями. Главная роль этой библиотеки — дать разработчику шаблонный набор готовых стратегий. Конечно, вы можете написать свои собственные стратегии, если вдруг в этом появится необходимость.

Concurrent. Позволяет обрабатывать все события параллельно. Например, когда пользователь быстро нажимает на несколько кнопок в интерфейсе, все нажатия будут обработаны одновременно, не дожидаясь завершения предыдущих.

Sequential. Обработка событий происходит поочерёдно, гарантируя, что ни одно событие не начнёт обработку, пока предыдущее не завершится. Как в очереди в магазине — каждый следующий покупатель ждёт, пока обслужат предыдущего.

Restartable. Отменяет выполнение текущего события при поступлении нового, но не прерывает уже запущенный код. Однако дальнейшие вызовы emit игнорируются — состояние больше не обновляется. Представьте поисковую строку — когда пользователь быстро печатает, нет смысла выполнять поиск для каждой буквы, достаточно обработать только последний запрос.

Чтобы корректно обработать отмену, можно использовать emit.isDone — это свойство, позволяющее проверить текущий статус обработки события. Если true, логику можно прервать для оптимизации производительности.

Droppable. Обеспечивает обработку только первого события с игнорированием последующих до завершения текущей обработки. Как кнопка отправки формы — повторные нажатия игнорируются, пока не завершится первая отправка.

У этих стратегий нет преимуществ или недостатков. Их цель — помочь вам достичь желаемого результата. Если она приводит к ошибкам, подумайте о выборе другой стратегии или пересмотрите её использование.

Cubit: Упрощённая альтернатива Bloc

Cubit — это легковесная реализация паттерна BLoC, который управляет состоянием и позволяет обновлять его с помощью методов. Его основной отличительной чертой является использования методов вместо событий (Event).

1class CounterCubit extends Cubit<int> {
2  CounterCubit(super.initialState);
3
4  Future<void> increment() async {
5    final newState = state + 1;
6    await Future.delayed(const Duration(seconds: 1));
7    emit(newState);
8  }
9
10  Future<void> decrement() async {
11    final newState = state - 1;
12    await Future.delayed(const Duration(seconds: 1));
13    emit(newState);
14  }
15}

Cubit не имеет встроенного механизма контроля очереди событий, что может привести к проблемам с консистентностью состояния. Рекомендуется использовать его в случаях, когда порядок обработки событий не критичен для логики приложения.

Мониторинг и отладка с BlocObserver

BlocObserver — абстрактный класс, представляющий собой инструмент мониторинга состояний и событий в приложении. При наследовании и переопределении его методов вы получаете возможность отслеживать изменения состояния, обработку событий и возникающие ошибки во всех Bloc- и Cubit-компонентах приложения. Это поможет вам с отладкой, аналитикой и мониторингом приложения.

Методы мониторинга

Метод onCreate

  • Назначение — отслеживание инициализации экземпляров Bloc/Cubit.

  • Применение — логирование инициализации зависимостей.

1void onCreate(BlocBase<Object?> bloc) {
2  super.onCreate(bloc);
3  print('Создан новый Bloc: ${bloc.runtimeType}');
4}

Метод onEvent

  • Назначение — мониторинг входящих событий.

  • Применение — анализ потока событий.

1void onEvent(Bloc<Object?, Object?> bloc, Object? event) {
2  super.onEvent(bloc, event);
3  print('Событие в ${bloc.runtimeType}: $event');
4}

Метод onChange

  • Назначение — мониторинг изменений состояния.

  • Применение — анализ изменений состояния, сбор аналитических данных.

1void onChange(BlocBase<Object?> bloc, Change<Object?> change) {
2  super.onChange(bloc, change);
3  print(
4    'Изменение состояния ${bloc.runtimeType}: '
5    '${change.currentState}${change.nextState}',
6  );
7}

Метод onTransition

  • Назначение — мониторинг переходов состояния в Bloc.

  • Применение — анализ цепочек преобразования «Событие → Состояние».

1void onTransition(
2  Bloc<Object?, Object?> bloc,
3  Transition<Object?, Object?> transition,
4) {
5  super.onTransition(bloc, transition);
6  final event = transition.event;
7  final currentState = transition.currentState;
8  final nextState = transition.nextState;
9  print(
10    'Переход ${bloc.runtimeType}: $event '
11    'из $currentState в $nextState',
12  );
13}

Метод onError

  • Назначение — обработка ошибок в Bloc/Cubit.

  • Применение — регистрация ошибок, анализ исключений.

1void onError(BlocBase<Object?> bloc, Object error, StackTrace stackTrace) {
2  super.onError(bloc, error, stackTrace);
3  print('Ошибка в ${bloc.runtimeType}: $error \nSk: $stackTrace');
4}

Метод onClose

  • Назначение — мониторинг завершения работы Bloc/Cubit.

  • Применение — контроль освобождения ресурсов.

1void onClose(BlocBase<Object?> bloc) {
2  super.onClose(bloc);
3  print('Bloc ${bloc.runtimeType} закрыт');
4}

Подключение BlocObserver

Реализация класса-наблюдателя:

1class AppBlocObserver extends BlocObserver {
2  const AppBlocObserver();
3  // Реализация необходимых методов
4}

Регистрация наблюдателя:

1void main() {
2  Bloc.observer = const AppBlocObserver();
3  runApp(const MyApp());
4}

Пример интеграции с системой аналитики FirebaseCrashlytics:

1void onError(BlocBase<Object?> bloc, Object error, StackTrace stackTrace) {
2  FirebaseCrashlytics.instance.recordError(error, stackTrace);
3  super.onError(bloc, error, stackTrace);
4}

Примечание

Примечание

Подробнее о Firebase можно узнать в параграфе о Firebase.

Обработка ошибок в Bloc реализуется через метод addError:

1class CounterBloc extends Bloc<CounterEvent, CounterState> {
2  ...
3
4  Future<void> _increment(IncrementCounterEvent event, Emitter emit) async {
5    try {
6      final newState = state + 1;
7      await Future.delayed(const Duration(seconds: 1));
8      emit(newState);
9    } on Object catch (error, sk) {
10      // Отправляем нашу ошибку в observer
11      addError(error, sk);
12    }
13  }
14}

Если обработчик событий в Bloc не содержит конструкции try/catch для обработки ошибок, Bloc всё равно обеспечивает контроль над ошибкой. При возникновении ошибки Bloc автоматически перехватывает исключение и перенаправляет его в BlocObserver через метод onError. Параллельно с этим необработанное исключение (англ. unhandled exception) попадаёт в текущую зону выполнения (Zone), где может быть обработано глобальными обработчиками ошибок.

Таким образом, BlocObserver обеспечивает функционал для отладки, мониторинга и анализа работы всех Bloc/Cubit в приложении.

Интеграция с Flutter: flutter_bloc

flutter_bloc — это библиотека с набором виджетов, которые позволяют упростить взаимодействие нашего Bloc с пользовательским интерфейсом Flutter. Рассмотрим основные компоненты библиотеки.

BlocProvider: внедрение зависимостей

BlocProvider представляет собой специализированную версию Provider, оптимизированную для работы с Bloc и Cubit. Он обеспечивает управление жизненным циклом объектов в дереве виджетов.

Примечание

Примечание

Для понимания основ внедрения зависимостей рекомендуется ознакомиться с параграфом о Provider.

Функциональность BlocProvider:

Создание и внедрение экземпляров Bloc/Cubit в дерево виджетов:

1BlocProvider(
2  create: (context) => CounterBloc(),
3  child: const MyApp(),
4);

Доступ к экземпляру Bloc из дочерних виджетов:

1final bloc = context.read<CounterBloc>();
2bloc.add(const IncrementEvent());

Инстанс, который был создан через BlocProvider, автоматически закроется (вызовется метод close), когда виджет BlocProvider удалится из дерева виджетов.

Но иногда, возможно даже чаще, чем вы можете себе представить, наши инстансы будут создаваться в другом месте, и тогда необходимо использовать другой способ внедрения нашего Bloc в дерево. Для этого используется BlocProvider.value.

Альтернативный метод внедрения существующих экземпляров:

1final bloc = CounterBloc();
2
3BlocProvider.value(
4  value: bloc,
5  child: const MyApp(),
6);

При использовании BlocProvider.value необходимо обеспечить освобождение ресурсов:

1void dispose() {
2  bloc.close();
3  super.dispose();
4}

Для управления множественными зависимостями используется MultiBlocProvider:

1MultiBlocProvider(
2  providers: [
3    BlocProvider(create: (context) => CounterBloc(), lazy: false),
4    BlocProvider.value(value: anotherBloc),
5  ],
6  child: const MyApp(),
7);

Виджет BlocProvider имеет конфигурационное поле lazy, которое по умолчанию установлено в значение true. Данный параметр определяет стратегию инициализации: при значении true создание экземпляра Bloc откладывается до момента первого обращения к нему.

Виджеты для работы с UI

Виджеты BlocListener, BlocBuilder, BlocSelector и BlocConsumer обеспечивают автоматический доступ к Bloc через дерево виджетов:

1BlocProvider(
2  create: (context) => CounterBloc(),
3  child: BlocListener<CounterBloc, CounterState>(
4    listener: ...,
5    child: ...,
6  ),
7)

Возможно явно указать экземпляр Bloc:

1final bloc = CounterBloc();
2
3BlocListener<CounterBloc, CounterState>(
4  bloc: bloc,
5  listener: (context, state) { ... },
6  child: ...,
7)

Теперь давайте рассмотрим каждый из этих виджетов подробно, чтобы понять их особенности и возможности использования.

BlocListener: обработка состояний

BlocListener предназначен для выполнения действий в ответ на изменения состояния без перестроения интерфейса.

1BlocListener<TaskBloc, TaskState>(
2  listener: (context, state) {
3    if (state is TaskSuccess) {
4      ScaffoldMessenger.of(context).showSnackBar(
5        SnackBar(content: Text('Message: ${state.message}')),
6      );
7    }
8  },
9  child: const TaskWidget(),
10);

Пример обработки исключений:

1if (state is TaskFailure) {
2  showDialog(...);
3}

Фильтрация состояний с помощью параметра listenWhen:

1BlocListener<TaskBloc, TaskState>(
2  listenWhen: (prevState, newState) {
3    return newState is SpecificState;
4  },
5  listener: (context, state) {
6    // Обработка состояния
7  },
8  child: const TaskWidget(),
9);

BlocBuilder: обновление интерфейса

BlocBuilder обеспечивает перестроение виджета при изменении состояния:

1BlocBuilder<TaskBloc, TaskState>(
2  builder: (context, state) => TaskWidget(isActive: state.isActive),
3);

BlocBuilder содержит под капотом BlocListener, который в коллбэке listener вызывает метод setState, тем самым наш интерфейс перестраивается при изменении состояния. BlocBuilder обладает всеми теми же возможностями, какими обладал BlocListener. Если мы хотим, чтобы наш виджет перестраивался в определённых состояниях, можем указать параметр buildWhen:

1BlocBuilder<TaskBloc, TaskState>(
2  buildWhen: (prevState, newState) => prevState.isActive != newState.isActive,
3  builder: (context, state) => TaskWidget(isActive: state.isActive),
4);

BlocConsumer: комплексный подход

BlocConsumer объединяет функциональность BlocListener и BlocBuilder:

1BlocConsumer<TaskBloc, TaskState>(
2  listenWhen: (prevState, newState) => newState is TaskSuccess,
3  buildWhen: (prevState, newState) => prevState.isActive != newState.isActive,
4  listener: (context, state) {
5    if (state is TaskSuccess) {
6      ScaffoldMessenger.of(context).showSnackBar(
7        SnackBar(content: Text('Message: ${state.message}')),
8      );
9    }
10  },
11  builder: (context, state) => TaskWidget(isActive: state.isActive),
12);

BlocSelector: оптимизация обновлений

BlocSelector оптимизирует перестроение интерфейса, отслеживая определённые параметры состояния:

1BlocSelector<TaskBloc, TaskState, bool>(
2  selector: (state) => state.isActive,
3  builder: (context, isActive) => TaskWidget(isActive: isActive),
4);

В завершение давайте подытожим ключевые моменты, о которых говорили:

  • Библиотека bloc — инструмент для управления состоянием, основанный на паттерне BLoC, который помогает структурировать логику приложения и сделать код более поддерживаемым.

  • Библиотека flutter_bloc содержит такие компоненты, как BlocListener, BlocBuilder, BlocConsumer и BlocSelector, которые позволяют связывать бизнес-логику с пользовательским интерфейсом.

  • Библиотека bloc_concurrency предоставляет трансформеры для управления потоком событий.

Если вы хотите углубить свои знания, обязательно загляните в официальную документациюBloc — там вы найдёте множество полезных материалов и практических примеров.

А в следующем параграфе мы познакомимся с другим паттерном для управления состояния — Redux.

Чтобы добавить в заметки выделенный текст, нажмите Ctrl + E

Отмечайте параграфы как прочитанные, чтобы видеть свой прогресс обучения

Предыдущий параграф5.10. BLoC: разбор паттерна
Следующий параграф5.12. Redux: основы